Skip to content

Instantly share code, notes, and snippets.

@zerobias
Last active January 7, 2019 10:26
Show Gist options
  • Save zerobias/ab87786c22376efe7927db71d582e067 to your computer and use it in GitHub Desktop.
Save zerobias/ab87786c22376efe7927db71d582e067 to your computer and use it in GitHub Desktop.
graph dynamic updates
let gr = 0
function classifyNode(node) {
if (isFinite(parseInt(node, 36))) return {type: 'int', node, id: node}
if (node.startsWith('point_')) return {type: 'point', node, id: node.replace('point_', '')}
if (node.startsWith('before_')) return {type: 'before', node, id: node.replace('before_', '')}
if (node.startsWith('after_')) return {type: 'after', node, id: node.replace('after_', '')}
throw new Error(`unknown node ${node}`)
}
window.processGraph = function processGraph(graph) {
const nodes = Object.keys(graph)
const classified = nodes.map(classifyNode)
const baseNodes = classified.map(
({node, type, id}) => ({
id: node,
group: parseInt(id, 36) * (type === 'int' ? 0 : 1),
label: `${type} ${id}`,
level: type === 'int'
? (id === '-1' ? 1 : 2)
: (type === 'before' ? 1 : 2)
})
)
const baseLinks = []
for (const cl of classified) {
const source = cl.node
for (const target of graph[source]) {
const clt = classifyNode(target)
let strength
if (clt.id === cl.id) {
strength = 0.07
} else if (cl.type === 'int') {
strength = 1.05
} else {
strength = 0.01
}
baseLinks.push({source, target, strength})
}
}
return {baseNodes, baseLinks}
}
window.graphHandlers = {}
window.graphData = window.processGraph({
//'-1': ['1', 'before_1'],
'1': [],
'4': [ '1', 'b' ],
'5': [],
'8': [ '5', 'v' ],
'11': [ 'w', '1z' ],
'12': [],
'15': [ '12', '11', '1a' ],
'20': [],
'25': [ '20', '2r', '2n', '2x' ],
'26': [],
'29': [ '26', '25', '2e' ],
'31': [ '2y', '2x', '36' ],
'36': [ '2x' ],
'54': [ '4z', '5x', '6h' ],
'55': [],
'58': [ '55', '54', '5d' ],
'63': [ '5y', '71' ],
'64': [],
'67': [ '64', '63', '6c' ],
'71': [],
'72': [],
'77': [ '72', '8d', '89', '8j' ],
'78': [],
'80': [ '7r' ],
'85': [],
'86': [],
'89': [ '86', '8x' ],
'93': [ '8y' ],
'94': [],
'97': [ '94', '93', '9c' ],
before_1: [ 'point_1' ],
point_1: [ 'after_1' ],
after_1: [ 'before_3' ],
before_2: [ 'point_2' ],
point_2: [ 'after_2' ],
after_2: [ 'before_8' ],
before_3: [ 'point_3' ],
point_3: [ 'after_3' ],
after_3: [ 'before_2' ],
b: [ '8' ],
before_4: [ 'point_4' ],
point_4: [ 'after_4' ],
after_4: [ 'before_d', 'before_n' ],
h: [ 'c', '1f', '2j' ],
c: [],
l: [ 'i', 'h', 'q' ],
i: [],
before_5: [ 'point_5' ],
point_5: [ 'after_5' ],
after_5: [ 'before_6', 'before_7' ],
before_6: [ 'point_6' ],
point_6: [ 'after_6' ],
after_6: [ 'before_4' ],
q: [ 'h' ],
before_7: [ 'point_7' ],
point_7: [ 'after_7' ],
after_7: [ 'before_4' ],
v: [ 'h' ],
before_8: [ 'point_8' ],
point_8: [ 'after_8' ],
after_8: [ 'before_4' ],
before_9: [ 'point_9' ],
point_9: [ 'after_9' ],
after_9: [ 'before_i' ],
w: [],
before_a: [ 'point_a' ],
point_a: [ 'after_a' ],
after_a: [ 'before_b', 'before_c' ],
before_b: [ 'point_b' ],
point_b: [ 'after_b' ],
after_b: [ 'before_9' ],
'1a': [ '11' ],
before_c: [ 'point_c' ],
point_c: [ 'after_c' ],
after_c: [ 'before_9' ],
'1f': [],
before_d: [ 'point_d' ],
point_d: [ 'after_d' ],
after_d: [ 'before_9' ],
before_e: [ 'point_e' ],
point_e: [ 'after_e' ],
after_e: [ 'before_u' ],
'1l': [ '1g', '2p', '2n', '2x' ],
'1g': [],
'1p': [ '1m', '1l', '1u' ],
'1m': [],
before_f: [ 'point_f' ],
point_f: [ 'after_f' ],
after_f: [ 'before_g', 'before_h' ],
before_g: [ 'point_g' ],
point_g: [ 'after_g' ],
after_g: [ 'before_e' ],
'1u': [ '1l' ],
before_h: [ 'point_h' ],
point_h: [ 'after_h' ],
after_h: [ 'before_e' ],
'1z': [],
before_i: [ 'point_i' ],
point_i: [ 'after_i' ],
after_i: [ 'before_e' ],
before_j: [ 'point_j' ],
point_j: [ 'after_j' ],
after_j: [ 'before_v' ],
before_k: [ 'point_k' ],
point_k: [ 'after_k' ],
after_k: [ 'before_l', 'before_m' ],
before_l: [ 'point_l' ],
point_l: [ 'after_l' ],
after_l: [ 'before_j' ],
'2e': [ '25' ],
before_m: [ 'point_m' ],
point_m: [ 'after_m' ],
after_m: [ 'before_j' ],
'2j': [],
before_n: [ 'point_n' ],
point_n: [ 'after_n' ],
after_n: [ 'before_j' ],
'2n': [ '2k', '3b' ],
'2k': [],
before_o: [ 'point_o' ],
point_o: [ 'after_o' ],
after_o: [ 'before_t' ],
'2p': [],
'2r': [],
before_p: [ 'point_p' ],
point_p: [ 'after_p' ],
after_p: [ 'before_10' ],
'2x': [ '2s', '3v' ],
'2s': [],
'2y': [],
before_q: [ 'point_q' ],
point_q: [ 'after_q' ],
after_q: [ 'before_r', 'before_s' ],
before_r: [ 'point_r' ],
point_r: [ 'after_r' ],
after_r: [ 'before_p' ],
before_s: [ 'point_s' ],
point_s: [ 'after_s' ],
after_s: [ 'before_p' ],
'3b': [ '2x' ],
before_t: [ 'point_t' ],
point_t: [ 'after_t' ],
after_t: [ 'before_p' ],
before_u: [ 'point_u' ],
point_u: [ 'after_u' ],
after_u: [ 'before_p' ],
before_v: [ 'point_v' ],
point_v: [ 'after_v' ],
after_v: [ 'before_p' ],
before_w: [ 'point_w' ],
point_w: [ 'after_w' ],
after_w: [ 'before_12' ],
'3h': [ '3c', '3x' ],
'3c': [],
'3l': [ '3i', '3h', '3q' ],
'3i': [],
before_x: [ 'point_x' ],
point_x: [ 'after_x' ],
after_x: [ 'before_y', 'before_z' ],
before_y: [ 'point_y' ],
point_y: [ 'after_y' ],
after_y: [ 'before_w' ],
'3q': [ '3h' ],
before_z: [ 'point_z' ],
point_z: [ 'after_z' ],
after_z: [ 'before_w' ],
'3v': [],
before_10: [ 'point_10' ],
point_10: [ 'after_10' ],
after_10: [ 'before_w' ],
'3x': [],
before_11: [ 'point_11' ],
point_11: [ 'after_11' ],
after_11: [],
before_12: [ 'point_12' ],
point_12: [ 'after_12' ],
after_12: [ 'before_11' ],
before_13: [ 'point_13' ],
point_13: [ 'after_13' ],
after_13: [ 'before_1b', 'before_1g' ],
'4z': [],
before_14: [ 'point_14' ],
point_14: [ 'after_14' ],
after_14: [ 'before_15', 'before_16' ],
before_15: [ 'point_15' ],
point_15: [ 'after_15' ],
after_15: [ 'before_13' ],
'5d': [ '54' ],
before_16: [ 'point_16' ],
point_16: [ 'after_16' ],
after_16: [ 'before_13' ],
before_17: [ 'point_17' ],
point_17: [ 'after_17' ],
after_17: [ 'before_1v' ],
'5j': [ '5e', '85' ],
'5e': [],
'5n': [ '5k', '5j', '5s' ],
'5k': [],
before_18: [ 'point_18' ],
point_18: [ 'after_18' ],
after_18: [ 'before_19', 'before_1a' ],
before_19: [ 'point_19' ],
point_19: [ 'after_19' ],
after_19: [ 'before_17' ],
'5s': [ '5j' ],
before_1a: [ 'point_1a' ],
point_1a: [ 'after_1a' ],
after_1a: [ 'before_17' ],
'5x': [],
before_1b: [ 'point_1b' ],
point_1b: [ 'after_1b' ],
after_1b: [ 'before_17' ],
before_1c: [ 'point_1c' ],
point_1c: [ 'after_1c' ],
after_1c: [ 'before_1l' ],
'5y': [],
before_1d: [ 'point_1d' ],
point_1d: [ 'after_1d' ],
after_1d: [ 'before_1e', 'before_1f' ],
before_1e: [ 'point_1e' ],
point_1e: [ 'after_1e' ],
after_1e: [ 'before_1c' ],
'6c': [ '63' ],
before_1f: [ 'point_1f' ],
point_1f: [ 'after_1f' ],
after_1f: [ 'before_1c' ],
'6h': [],
before_1g: [ 'point_1g' ],
point_1g: [ 'after_1g' ],
after_1g: [ 'before_1c' ],
before_1h: [ 'point_1h' ],
point_1h: [ 'after_1h' ],
after_1h: [ 'before_1q' ],
'6n': [ '6i', '7l' ],
'6i': [],
'6r': [ '6o', '6n', '6w' ],
'6o': [],
before_1i: [ 'point_1i' ],
point_1i: [ 'after_1i' ],
after_1i: [ 'before_1j', 'before_1k' ],
before_1j: [ 'point_1j' ],
point_1j: [ 'after_1j' ],
after_1j: [ 'before_1h' ],
'6w': [ '6n' ],
before_1k: [ 'point_1k' ],
point_1k: [ 'after_1k' ],
after_1k: [ 'before_1h' ],
before_1l: [ 'point_1l' ],
point_1l: [ 'after_1l' ],
after_1l: [ 'before_1h' ],
before_1m: [ 'point_1m' ],
point_1m: [ 'after_1m' ],
after_1m: [ 'before_23' ],
'7b': [ '78', '77', '7g' ],
before_1n: [ 'point_1n' ],
point_1n: [ 'after_1n' ],
after_1n: [ 'before_1o', 'before_1p' ],
before_1o: [ 'point_1o' ],
point_1o: [ 'after_1o' ],
after_1o: [ 'before_1m' ],
'7g': [ '77' ],
before_1p: [ 'point_1p' ],
point_1p: [ 'after_1p' ],
after_1p: [ 'before_1m' ],
'7l': [],
before_1q: [ 'point_1q' ],
point_1q: [ 'after_1q' ],
after_1q: [ 'before_1m' ],
before_1r: [ 'point_1r' ],
point_1r: [ 'after_1r' ],
after_1r: [ 'before_22' ],
'7r': [ '7m', '8b', '89', '8j' ],
'7m': [],
'7v': [ '7s', '7r', '80' ],
'7s': [],
before_1s: [ 'point_1s' ],
point_1s: [ 'after_1s' ],
after_1s: [ 'before_1t', 'before_1u' ],
before_1t: [ 'point_1t' ],
point_1t: [ 'after_1t' ],
after_1t: [ 'before_1r' ],
before_1u: [ 'point_1u' ],
point_1u: [ 'after_1u' ],
after_1u: [ 'before_1r' ],
before_1v: [ 'point_1v' ],
point_1v: [ 'after_1v' ],
after_1v: [ 'before_1r' ],
before_1w: [ 'point_1w' ],
point_1w: [ 'after_1w' ],
after_1w: [ 'before_21' ],
'8b': [],
'8d': [],
before_1x: [ 'point_1x' ],
point_1x: [ 'after_1x' ],
after_1x: [ 'before_28' ],
'8j': [ '8e', '9h' ],
'8e': [],
'8n': [ '8k', '8j', '8s' ],
'8k': [],
before_1y: [ 'point_1y' ],
point_1y: [ 'after_1y' ],
after_1y: [ 'before_1z', 'before_20' ],
before_1z: [ 'point_1z' ],
point_1z: [ 'after_1z' ],
after_1z: [ 'before_1x' ],
'8s': [ '8j' ],
before_20: [ 'point_20' ],
point_20: [ 'after_20' ],
after_20: [ 'before_1x' ],
'8x': [ '8j' ],
before_21: [ 'point_21' ],
point_21: [ 'after_21' ],
after_21: [ 'before_1x' ],
before_22: [ 'point_22' ],
point_22: [ 'after_22' ],
after_22: [ 'before_1x' ],
before_23: [ 'point_23' ],
point_23: [ 'after_23' ],
after_23: [ 'before_1x' ],
before_24: [ 'point_24' ],
point_24: [ 'after_24' ],
after_24: [],
'8y': [],
before_25: [ 'point_25' ],
point_25: [ 'after_25' ],
after_25: [ 'before_26', 'before_27' ],
before_26: [ 'point_26' ],
point_26: [ 'after_26' ],
after_26: [ 'before_24' ],
'9c': [ '93' ],
before_27: [ 'point_27' ],
point_27: [ 'after_27' ],
after_27: [ 'before_24' ],
'9h': [],
before_28: [ 'point_28' ],
point_28: [ 'after_28' ],
after_28: [ 'before_24' ],
'9l': [ '9i', '9s' ],
'9i': [],
before_29: [ 'point_29' ],
point_29: [ 'after_29' ],
after_29: [ 'before_2b' ],
'9p': [ '9m', '9x' ],
'9m': [],
before_2a: [ 'point_2a' ],
point_2a: [ 'after_2a' ],
after_2a: [ 'before_2c' ],
before_2b: [ 'point_2b' ],
point_2b: [ 'after_2b' ],
after_2b: [ 'before_2a' ],
'9s': [ '9p' ],
'9x': [ '54' ],
before_2c: [ 'point_2c' ],
point_2c: [ 'after_2c' ],
after_2c: [ 'before_13' ],
})
const baseNodes1 = [
{ id: "mammal", group: 0, label: "Mammals", level: 1 },
{ id: "dog" , group: 0, label: "Dogs" , level: 2 },
{ id: "cat" , group: 0, label: "Cats" , level: 2 },
{ id: "fox" , group: 0, label: "Foxes" , level: 2 },
{ id: "elk" , group: 0, label: "Elk" , level: 2 },
{ id: "insect", group: 1, label: "Insects", level: 1 },
{ id: "ant" , group: 1, label: "Ants" , level: 2 },
{ id: "bee" , group: 1, label: "Bees" , level: 2 },
{ id: "fish" , group: 2, label: "Fish" , level: 1 },
{ id: "carp" , group: 2, label: "Carp" , level: 2 },
{ id: "pike" , group: 2, label: "Pikes" , level: 2 }
];
const baseLinks1 = [
{ target: "mammal", source: "dog" , strength: 0.7 },
{ target: "mammal", source: "cat" , strength: 0.7 },
{ target: "mammal", source: "fox" , strength: 0.7 },
{ target: "mammal", source: "elk" , strength: 0.7 },
{ target: "insect", source: "ant" , strength: 0.7 },
{ target: "insect", source: "bee" , strength: 0.7 },
{ target: "fish" , source: "carp", strength: 0.7 },
{ target: "fish" , source: "pike", strength: 0.7 },
{ target: "cat" , source: "elk" , strength: 0.1 },
{ target: "carp" , source: "ant" , strength: 0.1 },
{ target: "elk" , source: "bee" , strength: 0.1 },
{ target: "dog" , source: "cat" , strength: 0.1 },
{ target: "fox" , source: "ant" , strength: 0.1 },
{ target: "pike" , source: "cat" , strength: 0.1 }
];
window.graphHandlers.dyn = function dyn({baseNodes, baseLinks} = window.graphData) {
const nodes = [...baseNodes];
let links = [...baseLinks];
function getNeighbors({id}) {
return baseLinks.reduce((neighbors, {target, source}) => {
if (target.id === id) {
neighbors.push(source.id)
} else if (source.id === id) {
neighbors.push(target.id)
}
return neighbors
},
[id]
);
}
function isNeighborLink({id}, {target, source}) {
return target.id === id || source.id === id;
}
function getNodeColor({id, level}, neighbors) {
if (Array.isArray(neighbors) && neighbors.includes(id)) {
return level === 1 ? 'blue' : 'green';
}
return level === 1 ? 'red' : 'gray';
}
function getLinkColor(node, link) {
return isNeighborLink(node, link) ? 'green' : '#E5E5E5'
}
function getTextColor({id}, neighbors) {
return Array.isArray(neighbors) && neighbors.includes(id) ? 'green' : 'black';
}
const width = window.innerWidth;
const height = window.innerHeight;
const svg = d3.select('svg');
svg.attr('width', width).attr('height', height)
let linkElements;
let nodeElements;
let textElements;
// we use svg groups to logically group the elements together
const linkGroup = svg.append('g').attr('class', 'links');
const nodeGroup = svg.append('g').attr('class', 'nodes');
const textGroup = svg.append('g').attr('class', 'texts');
// we use this reference to select/deselect
// after clicking the same element twice
let selectedId;
// simulation setup with all forces
const linkForce = d3
.forceLink()
.id(({id}) => id)
.strength(({strength}) => strength);
const simulation = d3
.forceSimulation()
.force('link', linkForce)
//.force('charge', d3.forceManyBody().strength(-120))
//.force('center', d3.forceCenter(width / 2, height / 2));
const dragDrop = d3.drag().on('start', node => {
node.fx = node.x
node.fy = node.y
}).on('drag', node => {
simulation.alphaTarget(0.7).restart()
node.fx = d3.event.x
node.fy = d3.event.y
}).on('end', node => {
if (!d3.event.active) {
simulation.alphaTarget(0)
}
node.fx = null
node.fy = null
});
// select node is called on every click
// we either update the data according to the selection
// or reset the data if the same node is clicked twice
function selectNode(selectedNode) {
if (selectedId === selectedNode.id) {
selectedId = undefined
resetData()
updateSimulation()
} else {
selectedId = selectedNode.id
updateData(selectedNode)
updateSimulation()
}
const neighbors = getNeighbors(selectedNode);
// we modify the styles to highlight selected nodes
nodeElements.attr('fill', node => getNodeColor(node, neighbors))
textElements.attr('fill', node => getTextColor(node, neighbors))
linkElements.attr('stroke', link => getLinkColor(selectedNode, link))
}
// this helper simple adds all nodes and links
// that are missing, to recreate the initial state
function resetData() {
const nodeIds = nodes.map(({id}) => id);
baseNodes.forEach(node => {
if (!nodeIds.includes(node.id)) {
nodes.push(node)
}
})
links = baseLinks
}
// diffing and mutating the data
function updateData(selectedNode) {
const neighbors = getNeighbors(selectedNode);
const newNodes = baseNodes.filter(({id, level}) => neighbors.includes(id) || level === 1);
const diff = {
removed: nodes.filter(node => !newNodes.includes(node)),
added: newNodes.filter(node => !nodes.includes(node))
};
diff.removed.forEach(node => { nodes.splice(nodes.indexOf(node), 1) })
diff.added.forEach(node => { nodes.push(node) })
links = baseLinks.filter(({target, source}) => target.id === selectedNode.id || source.id === selectedNode.id)
}
function updateGraph() {
// links
linkElements = linkGroup.selectAll('line')
.data(links, ({target, source}) => target.id + source.id)
linkElements.exit().remove()
const linkEnter = linkElements
.enter().append('line')
.attr('stroke-width', 1)
.attr('stroke', 'rgba(50, 50, 50, 0.2)');
linkElements = linkEnter.merge(linkElements)
// nodes
nodeElements = nodeGroup.selectAll('circle')
.data(nodes, ({id}) => id)
nodeElements.exit().remove()
const nodeEnter = nodeElements
.enter()
.append('circle')
.attr('r', 10)
.attr('fill', ({level}) => level === 1 ? 'red' : 'gray')
.call(dragDrop)
// we link the selectNode method here
// to update the graph on every click
.on('click', selectNode);
nodeElements = nodeEnter.merge(nodeElements)
// texts
textElements = textGroup.selectAll('text')
.data(nodes, ({id}) => id)
textElements.exit().remove()
const textEnter = textElements
.enter()
.append('text')
.text(({label}) => label)
.attr('font-size', 15)
.attr('dx', 15)
.attr('dy', 4);
textElements = textEnter.merge(textElements)
}
function updateSimulation() {
updateGraph()
simulation.nodes(nodes).on('tick', () => {
nodeElements
.attr('cx', ({x}) => x)
.attr('cy', ({y}) => y)
textElements
.attr('x', ({x}) => x)
.attr('y', ({y}) => y)
linkElements
.attr('x1', ({source}) => source.x)
.attr('y1', ({source}) => source.y)
.attr('x2', ({target}) => target.x)
.attr('y2', ({target}) => target.y)
})
simulation.force('link').links(links)
simulation.alphaTarget(0.7).restart()
}
// last but not least, we call updateSimulation
// to trigger the initial render
updateSimulation()
}
<!DOCTYPE html>
<meta charset="utf-8">
<svg width="1960" height="1600"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="api.js"></script>
<script src="data.js"></script>
<script src="dyn.js"></script>
<script src="index.js"></script>
window.graphHandlers.dyn()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment