2017-02-01 17:47:45 +00:00
|
|
|
// an array of synapses
|
|
|
|
// an array of topics
|
|
|
|
|
|
|
|
// a focal node
|
|
|
|
|
|
|
|
/*
|
|
|
|
step 1
|
|
|
|
generate an object/array that represents the intended layout
|
|
|
|
|
|
|
|
|
|
|
|
step 2
|
|
|
|
generate x,y coordinates for every topic in the layout object
|
|
|
|
|
|
|
|
step 3
|
|
|
|
set end states for every topic
|
|
|
|
|
|
|
|
Step 4
|
|
|
|
animate
|
|
|
|
*/
|
|
|
|
|
|
|
|
// synapses = [{ topic1_id: 4, topic2_id: 5, direction: 'from-to' }]
|
|
|
|
|
|
|
|
export const generateLayoutObject = (topics, synapses, focalTopicId) => {
|
|
|
|
let layout = [] // will be the final output
|
|
|
|
const usedTopics = {} // will store the topics that have been placed into islands
|
|
|
|
let newRoot
|
|
|
|
let currentTopic
|
|
|
|
|
|
|
|
const addParentsAndChildren = (topic) => {
|
|
|
|
if (!topic.id) return topic
|
|
|
|
|
|
|
|
usedTopics[topic.id] = true
|
|
|
|
|
|
|
|
topic.parents = []
|
|
|
|
topic.children = []
|
|
|
|
|
|
|
|
let filteredParentIds = synapses.filter(synapse => {
|
|
|
|
return synapse.topic2_id === topic.id
|
|
|
|
&& !usedTopics[synapse.topic1_id]
|
|
|
|
&& synapse.category === 'from-to'
|
|
|
|
}).map(synapse => synapse.topic1_id)
|
|
|
|
|
|
|
|
let filteredChildrenIds = synapses.filter(synapse => {
|
|
|
|
return synapse.topic1_id === topic.id
|
|
|
|
&& !usedTopics[synapse.topic2_id]
|
|
|
|
&& synapse.category === 'from-to'
|
|
|
|
|
|
|
|
}).map(synapse => synapse.topic2_id)
|
|
|
|
|
|
|
|
filteredParentIds.forEach(parentId => {
|
|
|
|
let parent = {
|
|
|
|
id: parentId
|
|
|
|
}
|
|
|
|
topic.parents.push(addParentsAndChildren(parent))
|
|
|
|
})
|
|
|
|
|
|
|
|
filteredChildrenIds.forEach(childId => {
|
|
|
|
let child = {
|
|
|
|
id: childId
|
|
|
|
}
|
|
|
|
topic.children.push(addParentsAndChildren(child))
|
|
|
|
})
|
|
|
|
|
|
|
|
return topic
|
|
|
|
}
|
|
|
|
|
|
|
|
// start with the focal node, and build its island
|
|
|
|
currentTopic = topics.find(t => t.id === focalTopicId)
|
|
|
|
if (!currentTopic) {
|
|
|
|
console.log('you didnt pass a valid focalTopicId')
|
|
|
|
return layout
|
|
|
|
}
|
|
|
|
newRoot = {
|
|
|
|
id: currentTopic.id
|
|
|
|
}
|
|
|
|
layout.push(addParentsAndChildren(newRoot))
|
|
|
|
|
|
|
|
//
|
|
|
|
// go through the the topics again, and build the island for the first topic that isn't
|
|
|
|
// yet in the usedTopics object (in any island). recurse
|
|
|
|
topics.forEach(topic => {
|
|
|
|
if (topic && topic.id && !usedTopics[topic.id]) {
|
|
|
|
newRoot = {
|
|
|
|
id: topic.id
|
|
|
|
}
|
|
|
|
layout.push(addParentsAndChildren(newRoot))
|
|
|
|
}
|
|
|
|
})
|
|
|
|
return layout
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const generateObjectCoordinates = (layoutObject, focalTopicId, focalCoords) => {
|
|
|
|
const coords = {}
|
|
|
|
|
|
|
|
const traverseIsland = (island, func, parent, child) => {
|
|
|
|
func(island, parent, child)
|
|
|
|
if (island.parents) {
|
|
|
|
island.parents.forEach(p => traverseIsland(p, func, null, island))
|
|
|
|
}
|
|
|
|
if (island.children) {
|
|
|
|
island.children.forEach(c => traverseIsland(c, func, island, null))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const positionTopic = (topic, parent, child) => {
|
|
|
|
if (topic.id === focalTopicId) {
|
|
|
|
// set the focalCoord to be what it already was
|
|
|
|
coords[topic.id] = focalCoords
|
|
|
|
} else if (!parent && !child) {
|
2017-02-01 18:08:02 +00:00
|
|
|
coords[topic.id] = {x: 0, y: 250}
|
2017-02-01 17:47:45 +00:00
|
|
|
} else if (parent) {
|
|
|
|
coords[topic.id] = {
|
|
|
|
x: coords[parent.id].x + 250,
|
2017-02-01 18:08:02 +00:00
|
|
|
y: coords[parent.id].y - (parent.id === focalTopicId ? 250 : 0)
|
2017-02-01 17:47:45 +00:00
|
|
|
}
|
|
|
|
} else if (child) {
|
|
|
|
coords[topic.id] = {
|
|
|
|
x: coords[child.id].x - 250,
|
|
|
|
y: coords[child.id].y
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// lay all of them out as if there were no other ones
|
|
|
|
layoutObject.forEach(island => {
|
|
|
|
traverseIsland(island, positionTopic)
|
|
|
|
})
|
|
|
|
|
|
|
|
// calculate the bounds of each island
|
|
|
|
|
|
|
|
// reposition the islands according to the bounds
|
|
|
|
return coords
|
|
|
|
}
|
|
|
|
|
|
|
|
export const getLayoutForData = (topics, synapses, focalTopicId, focalCoords) => {
|
|
|
|
return generateObjectCoordinates(generateLayoutObject(topics, synapses, focalTopicId), focalTopicId, focalCoords)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// if we've placed a node into an island, we need to NOT place it in any other islands
|
|
|
|
// Every node should only appear in one island
|
|
|
|
|
|
|
|
// the pseudo-focal node
|
|
|
|
|
|
|
|
|
|
|
|
// the top level array represents islands
|
|
|
|
// every island has some sort of 'focal' node
|
|
|
|
/*
|
|
|
|
var example = [
|
|
|
|
// the island that contains the focal node
|
|
|
|
{
|
|
|
|
id: 21,
|
|
|
|
parents: [
|
|
|
|
{
|
|
|
|
id: 25,
|
|
|
|
parents: []
|
|
|
|
},
|
|
|
|
{
|
|
|
|
id: 25,
|
|
|
|
parents: []
|
|
|
|
}
|
|
|
|
],
|
|
|
|
children: [{
|
|
|
|
id: 26,
|
|
|
|
children: []
|
|
|
|
}]
|
|
|
|
},
|
|
|
|
// all other islands should not contain children on the top level node
|
|
|
|
{
|
|
|
|
id: 21,
|
|
|
|
// parents may contain children
|
|
|
|
parents: [
|
|
|
|
{
|
|
|
|
id: 100,
|
|
|
|
parents: [
|
|
|
|
{
|
|
|
|
id: 101,
|
|
|
|
parents: [],
|
|
|
|
children: [
|
|
|
|
{
|
|
|
|
id: 103,
|
|
|
|
children: []
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
id: 102,
|
|
|
|
parents: []
|
|
|
|
}
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
id: 21,
|
|
|
|
parents: []
|
|
|
|
},
|
|
|
|
]
|
|
|
|
*/
|