51

我想生成这个:

在此处输入图像描述

使用这种数据结构(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描述了他将如何解决这个问题。我正在尝试这样做,但正如我发现的那样,这不是一件容易的事。

我的第一次尝试是从上到下按级别渲染。在这个更简单的尝试中,我基本上将所有的人按层嵌套并从上到下渲染。

我的第二次尝试是使用深度优先搜索使用其中一个祖先节点来渲染它。

我的主要问题是:我如何才能将这个答案实际应用到我目前拥有的东西上?在我的第二次尝试中,我尝试进行深度优先遍历,但我什至如何开始计算偏移网格所需的距离以使其与我想要生成的方式一致?

另外,我对深度优先理想的理解/实现,还是我可以以不同的方式遍历它?

在我的第二个示例中,节点显然重叠,因为我没有偏移/距离计算代码,但我对实际弄清楚如何开始这一点感到迷茫。

这是我制作的 walk 函数的描述,我正在尝试深度优先遍历:

// 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. 只有一个“根”节点,即起始节点。

注意:familyecho.com 似乎“隐藏”了一个分支,如果有很多叶节点并且存在冲突。可能需要实现这一点。

4

5 回答 5

15

尽管已发布(并接受)答案,但我认为发布昨晚解决此问题的内容并没有什么害处。

我更多地从新手的角度来解决这个问题,而不是使用现有的图/树遍历算法。

我的第一次尝试是从上到下按级别渲染。在这个更简单的尝试中,我基本上将所有的人按层嵌套并从上到下渲染。

这也正是我的第一次尝试。您可以自上而下、自下而上或从根开始遍历树。当您受到特定网站的启发时,从根目录开始似乎是一个合乎逻辑的选择。但是,我发现自下而上的方法更简单、更容易理解。

这是一个粗略的尝试:

绘制数据:

  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用来简化在作为合作伙伴的两个父母之间绘制连接器的地方。

  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);
}

我使用了两种不同的连接器,一种用于连接合作伙伴的水平连接器,一种用于连接亲子关系的倾斜连接器。这对我来说是一个非常棘手的部分,即绘制倒置的]水平连接器。这就是为什么要保持简单,我只是旋转了一个 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. 让父母集中协调孩子,尤其是多个孩子。目前,我的尝试只是将所有内容按顺序推到正确的位置。

希望能帮助到你。并将其张贴在这里,以便我在需要时也可以参考。

于 2016-01-19T11:16:02.733 回答
11

正如您展示的那样,您的树数据将不允许您绘制图表。实际上,您在那里缺少一些信息:

  • 树实际上应该是一个将 id 映射到人员数据的对象(字典)。否则,从 id(例如给出的children)返回到子数据的成本很高。
  • 由于孩子与父母双方相关联,因此存在重复信息。这实际上导致您发送的示例中的数据不正确('daugher1'是'wife'的孩子,但'me'的父母,'mary'可能是'jeff'的母亲;jessie是robert的伴侣;雷蒙德和贝蒂也是)

因此,在我的尝试(https://jsfiddle.net/61q2ym7q/)中,我将您的树转换为图形,然后进行各个阶段的计算以实现布局。

这是受 Sugiyama 算法的启发,但经过简化,因为该算法实现起来非常棘手。尽管如此,不同的阶段是:

  • 使用深度优先搜索将节点组织成层。我们分两步执行此操作,确保父母始终位于其父母之上的一层,然后在子与父母之间存在多个层时尝试缩短链接。这是我没有使用精确的 Sugiyama 算法的部分,它使用了复杂的切点概念。

  • 然后将节点分类到每一层以最小化边缘的交叉。我为此使用重心方法

  • 最后,在保留上述顺序的同时,为每个节点分配一个特定的 x 坐标,再次使用重心方法

这段代码有很多可以改进的地方(效率,例如通过合并一些循环)以及最终布局。但我试图让它更简单,以便更容易理解......

于 2016-01-18T11:51:27.480 回答
10

这与 Sugiyama 算法用于布局类层次结构的方式相距不远,因此您可能需要查看讨论该问题的论文这里有一本书章节涵盖了 Sugiyama 和其他分层布局算法。

我会独立布置树的上半部分和下半部分。关于上半部分要认识到的是,在其完全填充的形式中,它是 2 的所有幂,所以你有两个父母,四个祖父母,十六个曾祖父母,等等。

当您进行深度优先搜索时,用 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”建立您的密钥而第二大孩子的老大是“21”等。

你可以使用像 cola.js 这样的图形库来做到这一点,但你只会使用它的一小部分功能和一些你想要的风格元素(例如,让父亲和母亲靠得很近),可能需要添加分开,所以我怀疑它很容易从头开始构建,除非您需要库中的其他功能。

说到样式,习惯上为父连接器使用不同的线条样式(传统上它是双线)。此外,您不希望将“Mistress”节点布置在“me”/“wife”边缘之上。

ps 使用固定大小的节点,您可以为坐标系使用简单的网格。

于 2016-01-04T18:15:23.853 回答
5

这不是一个微不足道的问题,它涉及到图形绘制算法的大量研究。

这个问题最突出的方法是通过约束满足。但是不要试图自己实现它(除非你想学习新东西并花几个月的时间调试)

我不能高度推荐这个库:cola.jsGitHub

可能非常接近您需要的特定示例是网格布局。

于 2016-01-03T21:26:52.270 回答
5

从我所看到的 - 不看你那里的代码(现在) - 你有一个DAG(视觉表示是另一回事,现在我只谈论数据结构)。每个节点最多有 2 个传入连接,并且对去往其他节点的连接没有限制(一个可以有任意数量的子节点,但我们有关于每个人/节点最多 2 个父节点的信息)。

话虽如此,会有没有父母的节点(在这种情况下是“约翰”、“雷蒙德”、“贝蒂”、“情妇 1”、“妻子 1”和“女儿 1 男朋友”)。如果您从这些节点开始在图上执行BFS - 这将构成级别 0 - 您将获得每个级别的节点。但是,必须即时更新正确的级别。

关于视觉表示,我不是专家,但 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;
}

在过程结束时,您将拥有一个节点矩阵,其中 rowi包含 level 的节点i。您只需在 gridview(或您选择的任何布局)中表示它们。

让我知道是否有任何不清楚的地方。

于 2016-01-18T14:21:24.940 回答