これを生成したい:
このデータ構造では(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 );
});
});
}
});
}
}
要件/制限:
注:リーフノードがたくさんあり、衝突がある場合、familyecho.comはブランチを「非表示」にしているようです。これを実装する必要があるかもしれません。
回答は投稿されました(そして受け入れられました)が、昨夜この問題に取り組んだことを投稿しても害はないと思いました。
私は、グラフ/ツリートラバーサルの既存のアルゴリズムを使用するのではなく、初心者の観点からこの問題に取り組みました。
私の最初の試みは、これを上から下のレベルでレンダリングすることです。このより単純な試みでは、基本的にすべての人をレベルごとにネストし、これを上から下にレンダリングします。
これはまさに私の最初の試みでもありました。ツリーをトップダウン、ボトムアップ、またはルートからトラバースできます。あなたは特定のウェブサイトに触発されているので、ルートから始めるのは論理的な選択のようです。ただし、ボトムアップアプローチの方が簡単で理解しやすいことがわかりました。
これが大雑把な試みです:
レベルをキャッシュし、それを使用してツリーを上っていきます。
// 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
に基づいてデータからオブジェクトを取得します。
これが私たちがパートナーをプロットする方法です:
/* 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つの親間のコネクタのプロットを簡略化します。
ここで、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に追加します。
重要なものは次のとおりです。
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を回転させて、角度の付いたコネクタのように見せました。
これは、フィドルデモを含む完全なコードです。
フィドルデモ: 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> <button id="sample">Load Sample</button>
<br/><button id="download">Download Data</button>
</fieldset>
<fieldset id="downloadLink"></fieldset>
</div>
</div>
<div id="tree"></div>
これはすべて非常に大雑把な試みであり、最適化されていない試みであることは疑いの余地がありません。私が特にできなかったことは次のとおりです。
[
または]
の形をした水平コネクタを反転させます。それが役に立てば幸い。そして、私も必要なときに参照できるように、ここに投稿してください。
あなたがそれを示すように、あなたのツリーデータはあなたが図を描くことを可能にしません。実際、そこにはいくつかの情報がありません。
children
で指定されているIDから子のデータに戻るには、コストがかかります。したがって、私の試み( https://jsfiddle.net/61q2ym7q/ )では、ツリーをグラフに変換し、さまざまな計算段階を実行してレイアウトを実現しています。
これは杉山アルゴリズムに触発されていますが、そのアルゴリズムは実装が非常に難しいため、単純化されています。それでも、さまざまな段階は次のとおりです。
深さ優先探索を使用して、ノードをレイヤーに編成します。これは、親が常に親の上のレイヤーにあることを確認し、子と親の間に複数のレイヤーがある場合にリンクを短くすることによって、2つのステップで行います。これは、カットポイントの複雑な概念を使用する正確な杉山アルゴリズムを使用していない部分です。
次に、ノードを各レイヤーに並べ替えて、エッジの交差を最小限に抑えます。これには重心法を使用します
最後に、上記の順序を維持しながら、再び重心法を使用して、各ノードに特定のx座標を割り当てます。
このコード(たとえば、いくつかのループをマージすることによる効率)と最終的なレイアウトで改善できることがたくさんあります。しかし、私はそれをより簡単にフォローできるようにしようとしました...
これは、杉山アルゴリズムを使用してクラス階層をレイアウトする方法からそれほど遠くないので、それについて説明している 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.固定サイズのノードを使用すると、座標系の単純なグリッドを使用できます。
私が見ることができるものから-あなたがそこに持っているコードを見ずに(今のところ)-あなたは [〜#〜] 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
のノードが含まれるノードのマトリックスが作成されます。グリッドビュー(またはレイアウトとして選択したもの)でそれらを表す必要があります。
不明な点があれば教えてください。