Отображение динамически созданного семейного графа без перекрытия с использованием первого поиска глубины?

Я хочу сгенерировать это:

введите описание изображения здесь

С этой структурой данных (ids являются случайными, btw не последовательными):

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": [] },
];

Чтобы дать описание структуры данных, задан корневой/начальный node (me). Любой партнер (жена, бывший) находится на одном уровне. Все ниже становится уровнем -1, -2. Все, что указано выше, это уровень 1, 2 и т.д. Для родителей, братьев и сестер, детей и партнеров существуют свойства, определяющие идентификаторы для этого конкретного поля.

В предыдущем question, eh9 описал, как он это решит. Я пытаюсь это сделать, но, как я выяснил, это непростая задача.

Моя первая попытка показывает это по уровням сверху вниз. В этой более упрощенной попытке я в основном гнездю всех людей по уровням и отбрасываю их сверху вниз.

Моя вторая попытка дает это с одним из узлов-предков с использованием поиска глубины.

Мой главный вопрос:. Как я могу применить этот ответ к тому, что у меня есть? Во второй попытке я пытаюсь сделать первый проход, но как я могу даже начать учитывать расчёт, необходимый для смещения сеток, чтобы он соответствовал тому, как я хочу сгенерировать это?

Кроме того, мое понимание/реализация идеала глубины в первом случае, или я могу пройти это по-другому?

Узлы, очевидно, перекрываются в моем втором примере, поскольку у меня нет кода вычисления смещения/расстояния, но я теряюсь, чтобы понять, как я могу начать это.

Вот описание функции ходьбы, которую я сделал, где я пытаюсь выполнить первый проход глубины:

// 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 "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. Практически любые бизнес-правила, не определенные, могут быть приняты через это.
  • Разведение в семье не поддерживается, так как это слишком сложно. Не беспокойтесь об этом.
  • Поддерживаются несколько партнеров, поэтому это не так просто, как традиционное "семейное древо" с двумя родителями и одним ребенком.
  • Существует только один "корень" node, который является только стартовым node.

Примечания: familyecho.com, похоже, "скрывает" ветвь, если есть множество листовых узлов и есть столкновение. Возможно, потребуется выполнить это.

Ответ 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. По мере того как мы идем вверх, мы заимствуем node себя, своих родителей (рекурсивно) и своих партнеров. Партнерам по расписанию на самом деле не требуется, но я сделал это здесь, чтобы проложить коннекторы было легко. Если a node уже построено, мы просто пропустим эту часть.

Вот как мы строим партнеров:

/* 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. Вот как мы построим сам node:

Где, мы повторно используем координаты для каждого уникального уровня с помощью утилиты 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); 
}

Здесь, чтобы это было просто, я использовал очень грубое обнаружение столкновений, чтобы переместить узлы вправо, когда он уже существует. В сложном приложении это будет перемещать узлы динамически влево или вправо, чтобы поддерживать горизонтальный баланс.

Наконец, добавим, что 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>

Ответ 2

Как вы показываете, ваши данные дерева не позволят вам нарисовать диаграмму. На самом деле у вас отсутствует какая-то информация:

  • Дерево
  • должно действительно быть объектом (словарем), отображающим идентификатор для данных человека. В противном случае, стоить дорого из id, как указано в children, например, вернуться к дочерним данным.
  • существует дублируемая информация, так как дети связаны с обоими родителями. Это фактически приводит к неправильным данным в примере, который вы отправили ( "daugher1" - это ребенок "жены", но родитель "меня" и "mary" предположительно является матерью "jeff", jessie является партнером robert; так и raymond и betty)

В моей попытке (https://jsfiddle.net/61q2ym7q/), я поэтому конвертирую свое дерево в граф, а затем выполняю различные этапы вычислений для достижения макета.

Это вдохновляет алгоритм Sugiyama, но упрощается, поскольку этот алгоритм очень сложно реализовать. Тем не менее, различные этапы:

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

  • затем сортируйте узлы в каждом слое, чтобы свести к минимуму пересечение ребер. Я использую метод barycenter для этого

  • наконец, сохраняя вышеприведенный порядок, назначьте определенную координату x для каждого node, снова используя метод барицентра

В этом коде есть много вещей, которые могут быть улучшены (эффективность, например, слияние некоторых циклов), а также в конечном макете. Но я старался упростить, чтобы было легче следовать...

Ответ 3

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

Я выложил бы верхнюю и нижнюю половинки дерева независимо. Вещь, которую следует признать в верхней половине, состоит в том, что в ее полностью заполненной форме все силы двух, поэтому у вас есть два родителя, четыре бабушки и дедушки, шестнадцать прабабушек и т.д.

Как вы делаете свой поиск по глубине, помечайте каждый node а) его номер слоя и 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, но вы будете использовать только щель своей функциональности и некоторые стилистические элементы, которые вы хотите (например, держать отца и мать близко друг к другу), вероятно, понадобится добавляется отдельно, поэтому я подозреваю, что это легко построить с нуля, если вам не нужны другие функции из библиотеки.

Говоря о стиле, принято использовать другой стиль линии для родительского коннектора (традиционно это двойная линия). Кроме того, вы не хотите, чтобы "Хозяйка" node была выложена поверх края "я" / "жена".

p.s. С узлами фиксированного размера вы можете уйти с простой сеткой для вашей системы координат.

Ответ 4

Это не тривиальный вопрос, и он включает в себя большой объем исследований в алгоритмах графического рисования.

Наиболее заметным подходом к этой проблеме является удовлетворение ограничений. Но не пытайтесь реализовать это самостоятельно (если вы не хотите узнать что-то новое и потратить месячную отладку)

Я не могу рекомендовать эту библиотеку достаточно высоко: cola.js (GitHub)

Конкретный example, который может быть очень близок к тому, что вам нужно - это макет сетки.

Ответ 5

Из того, что я вижу - не глядя на код, который у вас есть (пока), вы имеете DAG (визуальное представление другое дело, теперь я говорю только о структуре данных). Каждый node имеет максимум 2 входящие соединения и не имеет ограничений на соединения, идущие на другие узлы (у каждого может быть произвольное количество детей, но у нас есть информация о максимум 2 родителях для каждого человека /node).

Говорят, что будут узлы, у которых нет родителей (в данном случае это "john", "raymond", "betty", "любовница 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;
}

В конце процедуры у вас будет матрица узлов, где строка i содержит узлы уровня i. Вы просто должны представлять их в gridview (или независимо от того, что вы выбираете как макет).

Сообщите мне, если что-то неясно.