web-dev-qa-db-ja.com

深さ優先探索を使用して、重複することなく動的に作成されたファミリグラフをレンダリングしますか?

これを生成したい:

enter image description here

このデータ構造では(IDはランダムですが、シーケンシャルではありません):

var tree = [
    { "id": 1, "name": "Me", "dob": "1988", "children": [4], "partners" : [2,3], root:true, level: 0, "parents": [5,6] },
    { "id": 2, "name": "Mistress 1", "dob": "1987", "children": [4], "partners" : [1], level: 0, "parents": [] },
    { "id": 3, "name": "Wife 1", "dob": "1988", "children": [5], "partners" : [1], level: 0, "parents": [] },
    { "id": 4, "name": "son 1", "dob": "", "children": [], "partners" : [], level: -1, "parents": [1,2] },
    { "id": 5, "name": "daughter 1", "dob": "", "children": [7], "partners" : [6], level: -1, "parents": [1,3] },
    { "id": 6, "name": "daughter 1s boyfriend", "dob": "", "children": [7], "partners" : [5], level: -1, "parents": [] },
    { "id": 7, "name": "son (bottom most)", "dob": "", "children": [], "partners" : [], level: -2, "parents": [5,6] },
    { "id": 8, "name": "jeff", "dob": "", "children": [1], "partners" : [9], level: 1, "parents": [10,11] },
    { "id": 9, "name": "maggie", "dob": "", "children": [1], "partners" : [8], level: 1, "parents": [] },
    { "id": 10, "name": "bob", "dob": "", "children": [8], "partners" : [11], level: 2, "parents": [12] },
    { "id": 11, "name": "mary", "dob": "", "children": [], "partners" : [10], level: 2, "parents": [] },
    { "id": 12, "name": "john", "dob": "", "children": [10], "partners" : [], level: 3, "parents": [] },
    { "id": 13, "name": "robert", "dob": "", "children": [9], "partners" : [], level: 2, "parents": [] },
    { "id": 14, "name": "jessie", "dob": "", "children": [9], "partners" : [], level: 2, "parents": [15,16] },
    { "id": 15, "name": "raymond", "dob": "", "children": [14], "partners" : [], level: 3, "parents": [] },
    { "id": 16, "name": "betty", "dob": "", "children": [14], "partners" : [], level: 3, "parents": [] },
];

データ構造を説明するために、ルート/開始ノード(me)が定義されています。すべてのパートナー(妻、元)は同じレベルにあります。以下のものはすべてレベル-1、-2になります。上記のものはすべてレベル1、2などです。親、兄弟、子およびパートナー特定のフィールドのIDを定義します。

私の以前の 質問 、eh9 説明 彼がこれをどのように解決するか。私はこれをやろうとしていますが、私が知ったように、それは簡単な作業ではありません。

私の 最初の試み はこれを上から下のレベルでレンダリングしています。このより単純な試みでは、基本的にすべての人をレベルごとにネストし、これを上から下にレンダリングします。

私の 2回目の試行 は、深さ優先探索を使用して、祖先ノードの1つでこれをレンダリングしています。

私の主な質問は:現在持っているものに実際にその答えを適用するにはどうすればよいですか? 2回目の試行では、深さ優先探索を実行しようとしていますが、グリッドをオフセットしてこれを生成する方法と一致させるために必要な距離の計算をどのように考慮し始めることができますか?

また、深さ優先の理想の理解/実装ですか、それともこれを別の方法でトラバースできますか?

オフセット/距離計算コードがないため、2番目の例ではノードが明らかに重複していますが、実際にそれを開始する方法を理解することができません。

深さ優先探索を試みている、私が作成したウォーク関数の説明は次のとおりです。

// this is used to map nodes to what they have "traversed". So on the first call of "john", dict would internally store this:
// dict.getItems() = [{ '12': [10] }]
// this means john (id=10) has traversed bob (id=10) and the code makes it not traverse if its already been traversed. 
var dict = new Dictionary;

walk( nested[0]['values'][0] ); // this calls walk on the first element in the "highest" level. in this case it's "john"

function walk( person, fromPersonId, callback ) {

    // if a person hasn't been defined in the dict map, define them
    if ( dict.get(person.id) == null ) {
        dict.set(person.id, []);


        if ( fromPersonId !== undefined || first ) {

            var div = generateBlock ( person, {
                // this offset code needs to be replaced
                top: first ? 0 : parseInt( $(getNodeById( fromPersonId ).element).css('top'), 10 )+50,
                left: first ? 0 : parseInt( $(getNodeById( fromPersonId ).element).css('left'), 10 )+50
            });

            //append this to the canvas
            $(canvas).append(div);

            person.element = div;
        }
    }

    // if this is not the first instance, so if we're calling walk on another node, and if the parent node hasn't been defined, define it
    if ( fromPersonId !== undefined ) {
        if ( dict.get(fromPersonId) == null ) {
            dict.set(fromPersonId, []);
        }

        // if the "caller" person hasn't been defined as traversing the current node, define them
        // so on the first call of walk, fromPersonId is null
        // it calls walk on the children and passes fromPersonId which is 12
        // so this defines {12:[10]} since fromPersonId is 12 and person.id would be 10 (bob)
        if ( dict.get(fromPersonId).indexOf(person.id) == -1 )
            dict.get(fromPersonId).Push( person.id );
    }

    console.log( person.name );

    // list of properties which house ids of relationships
    var iterable = ['partners', 'siblings', 'children', 'parents'];
    iterable.forEach(function(property) {
        if ( person[property] ) {
            person[property].forEach(function(nodeId) {
                // if this person hasnt been "traversed", walk through them
                if ( dict.get(person.id).indexOf(nodeId) == -1 )
                    walk( getNodeById( nodeId ), person.id, function() {
                        dict.get(person.id).Push( nodeId );
                    });
            });
        }
    });


}

}

要件/制限:

  1. これは編集者向けであり、familyecho.comに似ています。これにより、定義されていないほとんどすべてのビジネスルールを想定できます。
  2. 家族内での繁殖は、この方法が複雑になりすぎるため、サポートされていません。これについては心配しないでください。
  3. 複数のパートナーがサポートされているため、これは、2人の親と1人の子供だけの従来の「家系図」ほど簡単ではありません。
  4. 「ルート」ノードは1つだけで、これは開始ノードにすぎません。

:リーフノードがたくさんあり、衝突がある場合、familyecho.comはブランチを「非表示」にしているようです。これを実装する必要があるかもしれません。

48
meder omuraliev

回答は投稿されました(そして受け入れられました)が、昨夜この問題に取り組んだことを投稿しても害はないと思いました。

私は、グラフ/ツリートラバーサルの既存のアルゴリズムを使用するのではなく、初心者の観点からこの問題に取り組みました。

私の最初の試みは、これを上から下のレベルでレンダリングすることです。このより単純な試みでは、基本的にすべての人をレベルごとにネストし、これを上から下にレンダリングします。

これはまさに私の最初の試みでもありました。ツリーをトップダウン、ボトムアップ、またはルートからトラバースできます。あなたは特定のウェブサイトに触発されているので、ルートから始めるのは論理的な選択のようです。ただし、ボトムアップアプローチの方が簡単で理解しやすいことがわかりました。

これが大雑把な試みです:

データのプロット:

  1. 最下層から始めて、上に向かって進みます。エディターを介してそれを解決しようとしているという質問で述べたように、ツリーを構築するときに、関連するすべてのプロパティをオブジェクト配列に格納できます。

レベルをキャッシュし、それを使用してツリーを上っていきます。

// For all level starting from lowest one
levels.forEach(function(level) {
    // Get all persons from this level
    var startAt = data.filter(function(person) {
        return person.level == level;
    });
    startAt.forEach(function(start) {
        var person = getPerson(start.id);
        // Plot each person in this level
        plotNode(person, 'self');
        // Plot partners
        plotPartners(person);
        // And plot the parents of this person walking up
        plotParents(person);
    });
});

ここで、getPersonは、そのidに基づいてデータからオブジェクトを取得します。

  1. 歩きながら、ノード自体、その親(再帰的)、およびそのパートナーをプロットします。パートナーのプロットは実際には必要ありませんが、コネクタのプロットを簡単にするためにここで行いました。ノードがすでにプロットされている場合は、その部分をスキップします。

これが私たちがパートナーをプロットする方法です:

/* Plot partners for the current person */
function plotPartners(start) {
    if (! start) { return; }
    start.partners.forEach(function(partnerId) {
        var partner = getPerson(partnerId);
        // Plot node
        plotNode(partner, 'partners', start);
        // Plot partner connector
        plotConnector(start, partner, 'partners');
    });
}

そして、両親は再帰的に:

/* Plot parents walking up the tree */
function plotParents(start) {
    if (! start) { return; }
    start.parents.reduce(function(previousId, currentId) {
        var previousParent = getPerson(previousId), 
            currentParent = getPerson(currentId);
        // Plot node
        plotNode(currentParent, 'parents', start, start.parents.length);
        // Plot partner connector if multiple parents
        if (previousParent) { plotConnector(previousParent, currentParent, 'partners'); }
        // Plot parent connector
        plotConnector(start, currentParent, 'parents');
        // Recurse and plot parent by walking up the tree
        plotParents(currentParent);
        return currentId;
    }, 0);
}

reduceを使用して、パートナーとしての2つの親間のコネクタのプロットを簡略化します。

  1. これは、ノード自体をプロットする方法です。

ここで、findLevelユーティリティ関数を介して各一意のレベルの座標を再利用します。レベルのマップを維持し、それがtopの位置に到達することを確認します。休息は関係に基づいて決定されます。

/* Plot a single node */
function plotNode() {
    var person = arguments[0], relationType = arguments[1], relative = arguments[2], numberOfParents = arguments[3], 
        node = get(person.id), relativeNode, element = {}, thisLevel, exists 
    ;
    if (node) { return; }
    node = createNodeElement(person); 
    // Get the current level
    thisLevel = findLevel(person.level);
    if (! thisLevel) { 
        thisLevel = { 'level': person.level, 'top': startTop }; 
        levelMap.Push(thisLevel); 
    }
    // Depending on relation determine position to plot at relative to current person
    if (relationType == 'self') {
        node.style.left = startLeft + 'px'; 
        node.style.top = thisLevel.top + 'px';
    } else {
        relativeNode = get(relative.id);
    }
    if (relationType == 'partners') {
        // Plot to the right
        node.style.left = (parseInt(relativeNode.style.left) + size + (gap * 2)) + 'px';    
        node.style.top = parseInt(relativeNode.style.top) + 'px'; 
    }
    if (relationType == 'children') {
        // Plot below
        node.style.left = (parseInt(relativeNode.style.left) - size) + 'px';    
        node.style.top = (parseInt(relativeNode.style.top) + size + gap) + 'px';                            
    }
    if (relationType == 'parents') {
        // Plot above, if single parent plot directly above else plot with an offset to left
        if (numberOfParents == 1) { 
            node.style.left = parseInt(relativeNode.style.left) + 'px'; 
            node.style.top = (parseInt(relativeNode.style.top) - gap - size) + 'px';                        
        } else {
            node.style.left = (parseInt(relativeNode.style.left) - size) + 'px'; 
            node.style.top = (parseInt(relativeNode.style.top) - gap - size) + 'px';                                            
        }
    }

    // Avoid collision moving to right
    while (exists = detectCollision(node)) { 
        node.style.left = (exists.left + size + (gap * 2)) + 'px'; 
    }

    // Record level position
    if (thisLevel.top > parseInt(node.style.top)) {
        updateLevel(person.level, 'top', parseInt(node.style.top));
    }
    element.id = node.id; element.left = parseInt(node.style.left); element.top = parseInt(node.style.top); 
    elements.Push(element);

    // Add the node to the DOM tree
    tree.appendChild(node); 
}

ここでは簡単にするために、非常に大雑把な衝突検出を使用して、ノードがすでに存在する場合にノードを右に移動しました。非常に洗練されたアプリでは、これによりノードが動的に左または右に移動し、水平方向のバランスが維持されます。

最後に、そのノードをDOMに追加します。

  1. 残りはすべてヘルパー関数です。

重要なものは次のとおりです。

function detectCollision(node) {
    var element = elements.filter(function(elem) { 
        var left = parseInt(node.style.left);
        return ((elem.left == left || (elem.left < left && left < (elem.left + size + gap))) && elem.top == parseInt(node.style.top));
    });
    return element.pop();
}

上記は、ノード間のギャップを考慮した衝突の簡単な検出です。

そして、コネクタをプロットするには:

function plotConnector(source, destination, relation) {
    var connector = document.createElement('div'), orientation, start, stop, 
        x1, y1, x2, y2, length, angle, transform
    ; 
    orientation = (relation == 'partners') ? 'h' : 'v';
    connector.classList.add('asset');
    connector.classList.add('connector');
    connector.classList.add(orientation);
    start = get(source.id); stop = get(destination.id);
    if (relation == 'partners') {
        x1 = parseInt(start.style.left) + size; y1 = parseInt(start.style.top) + (size/2);
        x2 = parseInt(stop.style.left); y2 = parseInt(stop.style.top);
        length = (x2 - x1) + 'px';

        connector.style.width = length;
        connector.style.left = x1 + 'px';
        connector.style.top = y1 + 'px';
    }
    if (relation == 'parents') {
        x1 = parseInt(start.style.left) + (size/2); y1 = parseInt(start.style.top);
        x2 = parseInt(stop.style.left) + (size/2); y2 = parseInt(stop.style.top) + (size - 2);

        length = Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
        angle  = Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI;
        transform = 'rotate(' + angle + 'deg)'; 

        connector.style.width = length + 'px';
        connector.style.left = x1 + 'px';
        connector.style.top = y1 + 'px';
        connector.style.transform = transform;
    }
    tree.appendChild(connector);
}

パートナーを接続するための水平コネクタと、親子関係を接続するための角度付きコネクタの2つの異なるコネクタを使用しました。これは私にとって非常にトリッキーな部分であることがわかりました。つまり、反転した]水平コネクタをプロットすることです。これが単純な理由です。divを回転させて、角度の付いたコネクタのように見せました。

  1. ツリー全体が描画/プロットされると、負の位置が原因​​で画面外に移動するノードが存在する可能性があります(ボトムアップでトラバースしているため)。これを相殺するには、負の位置があるかどうかを確認してから、ツリー全体を下にシフトします。

これは、フィドルデモを含む完全なコードです。

フィドルデモ: http://jsfiddle.net/abhitalks/fvdw9xfq/embedded/result/


これは編集者向けで、次のようになります。

エディターの作成:

それが機能するかどうかをテストする最良の方法は、そのようなツリー/グラフをその場で作成し、それが正常にプロットされるかどうかを確認できるエディターを用意することです。

そこで、テスト用の簡単なエディターも作成しました。コードはまったく同じですが、エディターのルーチンに合うように少しリファクタリングされています。

エディターを使用したフィドルデモ: http://jsfiddle.net/abhitalks/56whqh0w/embedded/result

エディター付きスニペットデモ(全画面表示):

var sampleData = [
                { "id":  1, "name": "Me", "children": [4], "partners" : [2,3], root:true, level: 0, "parents": [8,9] },
                { "id":  2, "name": "Mistress", "children": [4], "partners" : [1], level: 0, "parents": [] },
                { "id":  3, "name": "Wife", "children": [5], "partners" : [1], level: 0, "parents": [] },
                { "id":  4, "name": "Son", "children": [], "partners" : [], level: -1, "parents": [1,2] },
                { "id":  5, "name": "Daughter", "children": [7], "partners" : [6], level: -1, "parents": [1,3] },
                { "id":  6, "name": "Boyfriend", "children": [7], "partners" : [5], level: -1, "parents": [] },
                { "id":  7, "name": "Son Last", "children": [], "partners" : [], level: -2, "parents": [5,6] },
                { "id":  8, "name": "Jeff", "children": [1], "partners" : [9], level: 1, "parents": [10,11] },
                { "id":  9, "name": "Maggie", "children": [1], "partners" : [8], level: 1, "parents": [13,14] },
                { "id": 10, "name": "Bob", "children": [8], "partners" : [11], level: 2, "parents": [12] },
                { "id": 11, "name": "Mary", "children": [], "partners" : [10], level: 2, "parents": [] },
                { "id": 12, "name": "John", "children": [10], "partners" : [], level: 3, "parents": [] },
                { "id": 13, "name": "Robert", "children": [9], "partners" : [14], level: 2, "parents": [] },
                { "id": 14, "name": "Jessie", "children": [9], "partners" : [13], level: 2, "parents": [15,16] },
                { "id": 15, "name": "Raymond", "children": [14], "partners" : [16], level: 3, "parents": [] },
                { "id": 16, "name": "Betty", "children": [14], "partners" : [15], level: 3, "parents": [] },
        ], 
        data = [], elements = [], levels = [], levelMap = [], 
        tree = document.getElementById('tree'), people = document.getElementById('people'), selectedNode, 
        startTop, startLeft, gap = 32, size = 64
;

/* Template object for person */
function Person(id) {
        this.id = id ? id : '';
        this.name = id ? id : '';
        this.partners = [];
        this.siblings = [];
        this.parents = [];
        this.children = [];
        this.level = 0;
        this.root = false;
}

/* Event listeners */
tree.addEventListener('click', function(e) {
        if (e.target.classList.contains('node')) {
                selectedNode = e.target; 
                select(selectedNode);
                document.getElementById('title').textContent = selectedNode.textContent;
                fillPeopleAtLevel();
        }
});
document.getElementById('save').addEventListener('click', function() {
        var pname = document.getElementById('pname').value;
        if (pname.length > 0) {
                data.forEach(function(person) {
                        if (person.id == selectedNode.id) {
                                person.name = pname;
                                selectedNode.textContent = pname;
                                document.getElementById('title').textContent = pname;
                        }
                });
        }
});
document.getElementById('add').addEventListener('click', function() {
        addPerson(document.getElementById('relation').value);
        plotTree();
}); 
document.getElementById('addExisting').addEventListener('click', function() {
        attachParent();
        plotTree();
}); 
document.getElementById('clear').addEventListener('click', startFresh); 
document.getElementById('sample').addEventListener('click', function() {
        data = sampleData.slice();
        plotTree();
}); 
document.getElementById('download').addEventListener('click', function() {
  if (data.length > 1) {
    var download = JSON.stringify(data, null, 4);
    var payload = "text/json;charset=utf-8," + encodeURIComponent(download);
    var a = document.createElement('a');
    a.href = 'data:' + payload;
    a.download = 'data.json';
    a.innerHTML = 'click to download';
    var container = document.getElementById('downloadLink');
    container.appendChild(a);
  }
}); 

/* Initialize */
function appInit() {
        // Approximate center of the div
        startTop = parseInt((tree.clientHeight / 2) - (size / 2)); 
        startLeft = parseInt((tree.clientWidth / 2) - (size / 2)); 
}

/* Start a fresh tree */
function startFresh() {
        var start, downloadArea = document.getElementById('downloadLink');
        // Reset Data Cache
        data = []; 
    appInit();
    while (downloadArea.hasChildNodes()) { downloadArea.removeChild(downloadArea.lastChild); }
        
        // Add a root "me" person to start with
        start = new Person('P01'); start.name = 'Me'; start.root = true;
        data.Push(start);
        
        // Plot the tree
        plotTree();
        
        // Pre-select the root node
        selectedNode = get('P01'); 
        document.getElementById('title').textContent = selectedNode.textContent;
}

/* Plot entire tree from bottom-up */
function plotTree() {
        // Reset other cache and DOM
        elements = [], levels = [], levelMap = []
        while (tree.hasChildNodes()) { tree.removeChild(tree.lastChild); }

        // Get all the available levels from the data
        data.forEach(function(elem) {
                if (levels.indexOf(elem.level) === -1) { levels.Push(elem.level); }
        });
        
        // Sort the levels in ascending order
        levels.sort(function(a, b) { return a - b; });

        // For all level starting from lowest one
        levels.forEach(function(level) {
                // Get all persons from this level
                var startAt = data.filter(function(person) {
                        return person.level == level;
                });
                startAt.forEach(function(start) {
                        var person = getPerson(start.id);
                        // Plot each person in this level
                        plotNode(person, 'self');
                        // Plot partners
                        plotPartners(person);
                        // And plot the parents of this person walking up
                        plotParents(person);
                });
                
        });
        
        // Adjust coordinates to keep the tree more or less in center
        adjustNegatives();
        
}

/* Plot partners for the current person */
function plotPartners(start) {
        if (! start) { return; }
        start.partners.forEach(function(partnerId) {
                var partner = getPerson(partnerId);
                // Plot node
                plotNode(partner, 'partners', start);
                // Plot partner connector
                plotConnector(start, partner, 'partners');
        });
}

/* Plot parents walking up the tree */
function plotParents(start) {
        if (! start) { return; }
        start.parents.reduce(function(previousId, currentId) {
                var previousParent = getPerson(previousId), 
                        currentParent = getPerson(currentId);
                // Plot node
                plotNode(currentParent, 'parents', start, start.parents.length);
                // Plot partner connector if multiple parents
                if (previousParent) { plotConnector(previousParent, currentParent, 'partners'); }
                // Plot parent connector
                plotConnector(start, currentParent, 'parents');
                // Recurse and plot parent by walking up the tree
                plotParents(currentParent);
                return currentId;
        }, 0);
}

/* Plot a single node */
function plotNode() {
        var person = arguments[0], relationType = arguments[1], relative = arguments[2], numberOfParents = arguments[3], 
                node = get(person.id), relativeNode, element = {}, thisLevel, exists 
        ;
        if (node) { return; }
        node = createNodeElement(person); 
        // Get the current level
        thisLevel = findLevel(person.level);
        if (! thisLevel) { 
                thisLevel = { 'level': person.level, 'top': startTop }; 
                levelMap.Push(thisLevel); 
        }
        // Depending on relation determine position to plot at relative to current person
        if (relationType == 'self') {
                node.style.left = startLeft + 'px'; 
                node.style.top = thisLevel.top + 'px';
        } else {
                relativeNode = get(relative.id);
        }
        if (relationType == 'partners') {
                // Plot to the right
                node.style.left = (parseInt(relativeNode.style.left) + size + (gap * 2)) + 'px';        
                node.style.top = parseInt(relativeNode.style.top) + 'px'; 
        }
        if (relationType == 'children') {
                // Plot below
                node.style.left = (parseInt(relativeNode.style.left) - size) + 'px';    
                node.style.top = (parseInt(relativeNode.style.top) + size + gap) + 'px';                                                        
        }
        if (relationType == 'parents') {
                // Plot above, if single parent plot directly above else plot with an offset to left
                if (numberOfParents == 1) { 
                        node.style.left = parseInt(relativeNode.style.left) + 'px'; 
                        node.style.top = (parseInt(relativeNode.style.top) - gap - size) + 'px';                                                
                } else {
                        node.style.left = (parseInt(relativeNode.style.left) - size) + 'px'; 
                        node.style.top = (parseInt(relativeNode.style.top) - gap - size) + 'px';                                                                                        
                }
        }
        
        // Avoid collision moving to right
        while (exists = detectCollision(node)) { 
                node.style.left = (exists.left + size + (gap * 2)) + 'px'; 
        }

        // Record level position
        if (thisLevel.top > parseInt(node.style.top)) {
                updateLevel(person.level, 'top', parseInt(node.style.top));
        }
        element.id = node.id; element.left = parseInt(node.style.left); element.top = parseInt(node.style.top); 
        elements.Push(element);
        
        // Add the node to the DOM tree
        tree.appendChild(node); 
}

/* Helper Functions */

function createNodeElement(person) {
        var node = document.createElement('div'); 
        node.id = person.id; 
        node.classList.add('node'); node.classList.add('asset'); 
        node.textContent = person.name; 
        node.setAttribute('data-level', person.level);
        return node;
}

function select(selectedNode) {
        var allNodes = document.querySelectorAll('div.node');
        [].forEach.call(allNodes, function(node) {
                node.classList.remove('selected');
        });
        selectedNode.classList.add('selected');
}

function get(id) { return document.getElementById(id); }

function getPerson(id) {
        var element = data.filter(function(elem) {
                return elem.id == id;
        });
        return element.pop();
}

function fillPeopleAtLevel() {
        if (!selectedNode) return;
        var person = getPerson(selectedNode.id), level = (person.level + 1), persons, option;
        while (people.hasChildNodes()) { people.removeChild(people.lastChild); }
        data.forEach(function(elem) {
                if (elem.level === level) {
                        option = document.createElement('option');
                        option.value = elem.id; option.textContent = elem.name;
                        people.appendChild(option);
                }
        });
        return persons;
}

function attachParent() {
        var parentId = people.value, thisId = selectedNode.id;
        updatePerson(thisId, 'parents', parentId);
        updatePerson(parentId, 'children', thisId);
}

function addPerson(relationType) {
        var newId = 'P' + (data.length < 9 ? '0' + (data.length + 1) : data.length + 1), 
                newPerson = new Person(newId), thisPerson;
        ;
        thisPerson = getPerson(selectedNode.id);
        // Add relation between originating person and this person
        updatePerson(thisPerson.id, relationType, newId);       
        switch (relationType) {
                case 'children': 
                        newPerson.parents.Push(thisPerson.id); 
                        newPerson.level = thisPerson.level - 1; 
                        break;
                case 'partners': 
                        newPerson.partners.Push(thisPerson.id); 
                        newPerson.level = thisPerson.level; 
                        break;
                case 'siblings': 
                        newPerson.siblings.Push(thisPerson.id); 
                        newPerson.level = thisPerson.level; 
                        // Add relation for all other relatives of originating person
                        newPerson = addRelation(thisPerson.id, relationType, newPerson);
                        break;
                case 'parents': 
                        newPerson.children.Push(thisPerson.id); 
                        newPerson.level = thisPerson.level + 1; 
                        break;
        }
        
        data.Push(newPerson);
}

function updatePerson(id, key, value) {
        data.forEach(function(person) {
                if (person.id === id) {
                        if (person[key].constructor === Array) { person[key].Push(value); }
                        else { person[key] = value; }
                }
        });
}

function addRelation(id, relationType, newPerson) {
        data.forEach(function(person) { 
                if (person[relationType].indexOf(id) != -1) {
                        person[relationType].Push(newPerson.id);
                        newPerson[relationType].Push(person.id);
                }
        });
        return newPerson;
}

function findLevel(level) {
        var element = levelMap.filter(function(elem) {
                return elem.level == level;
        });
        return element.pop();
} 

function updateLevel(id, key, value) {
        levelMap.forEach(function(level) {
                if (level.level === id) {
                        level[key] = value;
                }
        });
}

function detectCollision(node) {
        var element = elements.filter(function(elem) { 
                var left = parseInt(node.style.left);
                return ((elem.left == left || (elem.left < left && left < (elem.left + size + gap))) && elem.top == parseInt(node.style.top));
        });
        return element.pop();
}

function adjustNegatives() { 
        var allNodes = document.querySelectorAll('div.asset'), 
                minTop = startTop, diff = 0;
        for (var i=0; i < allNodes.length; i++) {
                if (parseInt(allNodes[i].style.top) < minTop) { minTop = parseInt(allNodes[i].style.top); }
        };
        if (minTop < startTop) {
                diff = Math.abs(minTop) + gap; 
                for (var i=0; i < allNodes.length; i++) {
                        allNodes[i].style.top = parseInt(allNodes[i].style.top) + diff + 'px';
                };
        }
}

function plotConnector(source, destination, relation) {
        var connector = document.createElement('div'), orientation, start, stop, 
                x1, y1, x2, y2, length, angle, transform
        ; 
        orientation = (relation == 'partners') ? 'h' : 'v';
        connector.classList.add('asset');
        connector.classList.add('connector');
        connector.classList.add(orientation);
        start = get(source.id); stop = get(destination.id);
        if (relation == 'partners') {
                x1 = parseInt(start.style.left) + size; y1 = parseInt(start.style.top) + (size/2);
                x2 = parseInt(stop.style.left); y2 = parseInt(stop.style.top);
                length = (x2 - x1) + 'px';
                
                connector.style.width = length;
                connector.style.left = x1 + 'px';
                connector.style.top = y1 + 'px';
        }
        if (relation == 'parents') {
                x1 = parseInt(start.style.left) + (size/2); y1 = parseInt(start.style.top);
                x2 = parseInt(stop.style.left) + (size/2); y2 = parseInt(stop.style.top) + (size - 2);
                
                length = Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
                angle  = Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI;
                transform = 'rotate(' + angle + 'deg)'; 
                
                connector.style.width = length + 'px';
                connector.style.left = x1 + 'px';
                connector.style.top = y1 + 'px';
                connector.style.transform = transform;
        }
        tree.appendChild(connector);
}
                
/* App Starts Here */
appInit();
startFresh();
* { box-sizing: border-box; padding: 0; margin: 0; }
html, body { width: 100vw; height: 100vh; overflow: hidden; font-family: sans-serif; font-size: 0.9em; }
#editor { float: left; width: 20vw; height: 100vh; overflow: hidden; overflow-y: scroll; border: 1px solid #ddd; }
#tree { float: left; width: 80vw; height: 100vh; overflow: auto; position: relative; }
h2 { text-align: center; margin: 12px; color: #bbb; }
fieldset { margin: 12px; padding: 8px 4px; border: 1px solid #bbb; }
legend { margin: 0px 8px; padding: 4px; }
button, input, select { padding: 4px; margin: 8px 0px;  }
button { min-width: 64px; }
div.node {
        width: 64px; height: 64px; line-height: 64px;
        background-color: #339; color: #efefef;
        font-family: sans-serif; font-size: 0.7em;
        text-align: center; border-radius: 50%; 
        overflow: hidden; position: absolute; cursor: pointer;
} 
div.connector { position: absolute; background-color: #333; z-index: -10; }
div.connector.h { height: 2px; background-color: #ddd; }
div.connector.v { height: 1px; background-color: #66d; -webkit-transform-Origin: 0 100%; transform-Origin: 0 100%; }
div[data-level='0'] { background-color: #933; }
div[data-level='1'], div[data-level='-1'] { background-color: #393; }
div[data-level='2'], div[data-level='-2'] { background-color: #333; }
div.node.selected { background-color: #efefef; color: #444; }
<div id="editor">
        <h2 id="title">Me</h2>
        <div>
                <fieldset>
                        <legend>Change Name</legend>
                        <label>Name: <input id="pname" type="text" /></label>
                        <br /><button id="save">Ok</button>
                </fieldset>
                <fieldset>
                        <legend>Add Nodes</legend>
                        <label for="relation">Add: </label>
                        <select id="relation">
                                <option value="partners">Partner</option>
                                <option value="siblings">Sibling</option>
                                <option value="parents">Parent</option>
                                <option value="children">Child</option>
                        </select>
                        <button id="add">Ok</button><br />
                        <label for="relation">Add: </label>
                        <select id="people"></select>
                        <button id="addExisting">As Parent</button>
                </fieldset>
                <fieldset>
                        <legend>Misc</legend>
                        <button id="clear">Clear</button>&nbsp;&nbsp;<button id="sample">Load Sample</button>
            <br/><button id="download">Download Data</button>
                </fieldset>
        <fieldset id="downloadLink"></fieldset>
        </div>
</div>
<div id="tree"></div>

これはすべて非常に大雑把な試みであり、最適化されていない試みであることは疑いの余地がありません。私が特にできなかったことは次のとおりです。

  1. 親子関係のために[または]の形をした水平コネクタを反転させます。
  2. ツリーを水平方向にバランスさせる。つまり、どちらが重い側であるかを動的に把握し、それらのノードを左にシフトします。
  3. 親を子供、特に複数の子供に関して一元的に調整させる。現在、私の試みは単にすべてを順番に正しくプッシュします。

それが役に立てば幸い。そして、私も必要なときに参照できるように、ここに投稿してください。

11
Abhitalks

あなたがそれを示すように、あなたのツリーデータはあなたが図を描くことを可能にしません。実際、そこにはいくつかの情報がありません。

  • ツリーは、実際にはIDを個人のデータにマッピングするオブジェクト(辞書)である必要があります。それ以外の場合、たとえばchildrenで指定されているIDから子のデータに戻るには、コストがかかります。
  • 子は両方の親に関連付けられているため、情報が重複しています。これは実際に送信した例の誤ったデータにつながります(「daugher1」は「wife」の子ですが、「me」の親であり、「mary」はおそらく「jeff」の母です。jessieはrobertのパートナーです。レイモンドとベティもそうです)

したがって、私の試み( https://jsfiddle.net/61q2ym7q/ )では、ツリーをグラフに変換し、さまざまな計算段階を実行してレイアウトを実現しています。

これは杉山アルゴリズムに触発されていますが、そのアルゴリズムは実装が非常に難しいため、単純化されています。それでも、さまざまな段階は次のとおりです。

  • 深さ優先探索を使用して、ノードをレイヤーに編成します。これは、親が常に親の上のレイヤーにあることを確認し、子と親の間に複数のレイヤーがある場合にリンクを短くすることによって、2つのステップで行います。これは、カットポイントの複雑な概念を使用する正確な杉山アルゴリズムを使用していない部分です。

  • 次に、ノードを各レイヤーに並べ替えて、エッジの交差を最小限に抑えます。これには重心法を使用します

  • 最後に、上記の順序を維持しながら、再び重心法を使用して、各ノードに特定のx座標を割り当てます。

このコード(たとえば、いくつかのループをマージすることによる効率)と最終的なレイアウトで改善できることがたくさんあります。しかし、私はそれをより簡単にフォローできるようにしようとしました...

10
manuBriot

これは、杉山アルゴリズムを使用してクラス階層をレイアウトする方法からそれほど遠くないので、それについて説明している papers を参照することをお勧めします。杉山と他の階層レイアウトアルゴリズムをカバーする本の章があります ここ

ツリーの上半分と下半分を個別にレイアウトします。上半分について認識すべきことは、完全に入力された形式では、すべて2の累乗であるため、2人の親、4人の祖父母、16人の曽祖父母などがいるということです。

深さ優先探索を行うときは、各ノードにa)レイヤー番号とb)照合順序のタグを付けます。データ構造には性別が含まれていません。文体上の理由と照合順序を理解するために、これが本当に必要です。幸い、すべての系図データには性別が含まれています。

父親には「A」、母親には「B」のタグを付けます。祖父母には別の手紙が追加されるので、次のようになります。

father jeff - A, layer 1
mother maggie - B, layer 1
paternal grandfather bob - AA, layer 2
paternal grandmother mary - AB, layer 2
paternal grandfather robert - BA, layer 2
paternal grandmother jessie - BB, layer 2
g-g-father john - AAA, layer 3
etc

移動しながら、各レイヤーのリストにノードを追加します。各レイヤーを性別キーで並べ替えます(並べ替えられたリストを使用しない場合)。番号が最も大きいレイヤーからレイアウトを開始し、ノードを左(AAAAA)から右(BBBBB)にレイアウトし、欠落しているノードにギャップを残します。様式的には、欠落しているノードの周りで折りたたむかどうか、折りたたむ場合はその量を決定します(ただし、最初に単純なバージョンを実装することをお勧めします)。

レイヤーを降順で配置します。位置の折りたたみ/調整がない場合は、下位レイヤーの位置を直接計算できます。調整する場合は、前のレイヤーの親の位置を参照し、その下の子を中央に配置する必要があります。

図の下半分も同様の方法で実行できますが、性別で並べ替える代わりに、出生順位で並べ替えて、そこからキーを作成することをお勧めします。長女の長男はキー「11」、2番目の長子の長男は「21」などです。

Cola.jsのようなグラフライブラリでこれを行うことはできますが、その機能のスライバーと必要なスタイル要素の一部(たとえば、父と母を近づける)のみを使用するため、おそらく追加する必要があります個別に作成するので、ライブラリの他の機能が必要でない限り、最初から作成するのも簡単だと思います。

スタイルと言えば、親コネクタに別のラインスタイルを使用するのが通例です(従来は二重線です)。また、「Mistress」ノードを「me」/「wife」エッジの上に配置する必要はありません。

p.s.固定サイズのノードを使用すると、座標系の単純なグリッドを使用できます。

9
Tom Morris

私が見ることができるものから-あなたがそこに持っているコードを見ずに(今のところ)-あなたは [〜#〜] dag [〜#〜] (視覚的表現は別の問題です、今私はデータ構造についてのみ話している)。各ノードには最大2つの着信接続があり、他のノードへの接続に制約はありません(1つには任意の数の子を含めることができますが、各個人/ノードの最大2つの親に関する情報があります)。

そうは言っても、親を持たないノードがあります(この場合、「john」、「raymond」、「betty」、「mistress 1」、「wife 1」、「daughter 1boyfriend」)。これらのノード(レベル0を構成する)から始まるグラフで [〜#〜] bfs [〜#〜] を実行すると、各レベルのノードが取得されます。ただし、正しいレベルはその場で更新する必要があります。

視覚的表現に関しては、私は専門家ではありませんが、IMOはグリッド(テーブルのようなもの)ビューを介して実現できます。各行には、特定のレベルのノードが含まれています。特定の行の要素は、同じ行、行x - 1、および行x + 1の他の要素との関係に基づいて配置されます。

アイデアをよりよく説明するために、いくつかの擬似コードを挿入する方が良いと思います(JSではありませんが、私の強みではありません):

getItemsByLevel(Graph graph)
{
    Node[,] result = new Node[,];
    var orphans = graph.getOrphans();
    var visiting = new HashMap();
    var visited = new HashMap();
    var queue = new Queue<Node>();

    queue.pushAll(orphans);

    while(!queue.isEmpty())
    {
        var currentNode = queue.pop();

        if(currentNode.relatedNodes.areNotBeingVisited()) // the nodes that should be on the same level
        {
            // the level of the current node was not right
            currentNode.level++;
            queue.Push(currentNode);
        }
        else
        {
            var children = currentNode.children;

            foreach(var child in children)
            {
                child.level = currentNode.level + 1;
                queue.Push(child);
            }

            visited.insert(currentNode);
            result[currentNode.level, lastOfRow] = currentNode;
        }
    }

    return result;
}

手順の最後に、行iにレベルiのノードが含まれるノードのマトリックスが作成されます。グリッドビュー(またはレイアウトとして選択したもの)でそれらを表す必要があります。

不明な点があれば教えてください。

5
Gentian Kasa

これは簡単な質問ではなく、グラフ描画アルゴリズムの研究の大規模なコーパスが含まれます。

この問題に対する最も顕著なアプローチは、制約の満足によるものです。ただし、これを自分で実装しようとしないでください(何か新しいことを学び、デバッグに数か月を費やしたい場合を除く)

このライブラリを十分に推奨することはできません: cola.jsGitHub

必要なものに非常に近い可能性のある特定の はグリッドレイアウトです。

5
Anvaka