forked from rofrischmann's block: Dynamic Updates
forked from anonymous's block: Force Directed Graph
xxxxxxxxxx
<meta charset="utf-8">
<svg width="960" height="600"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var baseNodes = [
{ id: "root", group: -1, label: "Curriculum", level: -1 },
{ id: "primary", group: 0, label: "Primary", level: 0 },
{ id: "secondary", group: 0, label: "Secondary", level: 0 },
{ id: "d1", group: 1, label: "Darjah 1", level: 1 },
{ id: "d2", group: 1, label: "Darjah 2", level: 1 },
{ id: "d3", group: 1, label: "Darjah 3", level: 1 },
{ id: "d4", group: 1, label: "Darjah 4", level: 1 },
{ id: "d5", group: 1, label: "Darjah 5", level: 1 },
{ id: "d6", group: 1, label: "Darjah 6", level: 1 },
{ id: "t1", group: 1, label: "Tingkatan 1", level: 1 },
{ id: "t2", group: 1, label: "Tingkatan 2", level: 1 },
{ id: "t3", group: 1, label: "Tingkatan 3", level: 1 },
{ id: "t4", group: 1, label: "Tingkatan 4", level: 1 },
{ id: "t5", group: 1, label: "Tingkatan 5", level: 1 },
{ id: "t6", group: 1, label: "Tingkatan 6", level: 1 },
{ id: "d1,d2:BI", group: 2, label: "Bahasa Inggeris", level: 2 },
{ id: "d3:BI", group: 2, label: "Bahasa Inggeris", level: 2 },
{ id: "d4:BI", group: 2, label: "Bahasa Inggeris", level: 2 },
{ id: "d5,d6:BI", group: 2, label: "Bahasa Inggeris", level: 2 },
{ id: "d1,d2:BI:grammar", group: 3, label: "Grammar", level: 3 },
{ id: "d1,d2:BI:listening-speaking", group: 3, label: "Listening & Speaking", level: 3 },
{ id: "d1,d2:BI:literature", group: 3, label: "Literature", level: 3 },
{ id: "d1,d2:BI:reading", group: 3, label: "Reading", level: 3 },
{ id: "d1,d2:BI:writing", group: 3, label: "Writing", level: 3 },
{ id: "d1,d2:BI:grammar", group: 3, label: "Grammar", level: 3 },
{ id: "d1,d2:BI:listening-speaking", group: 3, label: "Listening & Speaking", level: 3 },
{ id: "d1,d2:BI:literature", group: 3, label: "Literature", level: 3 },
{ id: "d1,d2:reading", group: 3, label: "Reading", level: 3 },
{ id: "d1,d2:writing", group: 3, label: "Writing", level: 3 },
{ id: "d3:BI:grammar", group: 3, label: "Grammar", level: 3 },
{ id: "d3:BI:listening-speaking", group: 3, label: "Listening & Speaking", level: 3 },
{ id: "d3:BI:literature", group: 3, label: "Literature", level: 3 },
{ id: "d3:BI:reading", group: 3, label: "Reading", level: 3 },
{ id: "d3:BI:writing", group: 3, label: "Writing", level: 3 },
{ id: "d4:BI:grammar", group: 3, label: "Grammar", level: 3 },
{ id: "d4:BI:listening-speaking", group: 3, label: "Listening & Speaking", level: 3 },
{ id: "d4:BI:literature", group: 3, label: "Literature", level: 3 },
{ id: "d4:BI:reading", group: 3, label: "Reading", level: 3 },
{ id: "d4:BI:writing", group: 3, label: "Writing", level: 3 },
{ id: "d5:BI:grammar", group: 3, label: "Grammar", level: 3 },
{ id: "d5:BI:listening-speaking", group: 3, label: "Listening & Speaking", level: 3 },
{ id: "d5:BI:literature", group: 3, label: "Literature", level: 3 },
{ id: "d5:BI:reading", group: 3, label: "Reading", level: 3 },
{ id: "d5:BI:writing", group: 3, label: "Writing", level: 3 },
{ id: "d6:BI:grammar", group: 3, label: "Grammar", level: 3 },
{ id: "d6:BI:listening-speaking", group: 3, label: "Listening & Speaking", level: 3 },
{ id: "d6:BI:literature", group: 3, label: "Literature", level: 3 },
{ id: "d6:BI:reading", group: 3, label: "Reading", level: 3 },
{ id: "d6:BI:writing", group: 3, label: "Writing", level: 3 },
{ id: "d1,d2:BM", group: 2, label: "Bahasa Melayu", level: 2 },
{ id: "d3:BM", group: 2, label: "Bahasa Melayu", level: 2 },
{ id: "d4:BM", group: 2, label: "Bahasa Melayu", level: 2 },
{ id: "d5,d6:BM", group: 2, label: "Bahasa Melayu", level: 2 },
{ id: "d6:BM", group: 2, label: "Bahasa Melayu", level: 2 },
{ id: "t1,t2:BI", group: 2, label: "Bahasa Inggeris", level: 2 },
{ id: "t3:BI", group: 2, label: "Bahasa Inggeris", level: 2 },
{ id: "t4:BI", group: 2, label: "Bahasa Inggeris", level: 2 },
{ id: "t5,t6:BI", group: 2, label: "Bahasa Inggeris", level: 2 },
{ id: "t1,t2:BM", group: 2, label: "Bahasa Melayu", level: 2 },
{ id: "t3:BM", group: 2, label: "Bahasa Melayu", level: 2 },
{ id: "t4:BM", group: 2, label: "Bahasa Melayu", level: 2 },
{ id: "t5,t6:BM", group: 2, label: "Bahasa Melayu", level: 2 },
{ id: "t6:BM", group: 2, label: "Bahasa Melayu", level: 2 }
];
var baseLinks = [
{ target: "primary", source: "root", strength: 1},
{ target: "secondary", source: "root", strength: 1},
{ target: "d1", source: "primary", strength: 1},
{ target: "d2", source: "primary", strength: 1},
{ target: "d3", source: "primary", strength: 1},
{ target: "d4", source: "primary", strength: 1},
{ target: "d5", source: "primary", strength: 1},
{ target: "d6", source: "primary", strength: 1},
{ target: "t1", source: "secondary", strength: 1},
{ target: "t2", source: "secondary", strength: 1},
{ target: "t3", source: "secondary", strength: 1},
{ target: "t4", source: "secondary", strength: 1},
{ target: "t5", source: "secondary", strength: 1},
{ target: "t6", source: "secondary", strength: 1},
{ target: "d1,d2:BI", source: "d1" , strength: 1 },
{ target: "d1,d2:BI", source: "d2" , strength: 1 },
{ target: "d3:BI", source: "d3" , strength: 1 },
{ target: "d4:BI", source: "d4" , strength: 1 },
{ target: "d5,d6:BI", source: "d5" , strength: 1 },
{ target: "d5,d6:BI", source: "d6" , strength: 1 },
{ target: "d1,d2:BI:grammar", source: "d1,d2:BI", strength: 1},
{ target: "d1,d2:BI:listening-speaking", source: "d1,d2:BI", strength: 1},
{ target: "d1,d2:BI:literature", source: "d1,d2:BI", strength: 1},
{ target: "d1,d2:BI:reading", source: "d1,d2:BI", strength: 1},
{ target: "d1,d2:BI:writing", source: "d1,d2:BI", strength: 1},
{ target: "d3:BI:grammar", source: "d3:BI", strength: 1},
{ target: "d3:BI:listening-speaking", source: "d3:BI", strength: 1},
{ target: "d3:BI:literature", source: "d3:BI", strength: 1},
{ target: "d3:BI:reading", source: "d3:BI", strength: 1},
{ target: "d3:BI:writing", source: "d3:BI", strength: 1},
{ target: "d1,d2:BM", source: "d1" , strength: 1 },
{ target: "d1,d2:BM", source: "d2" , strength: 1 },
{ target: "d3:BM", source: "d3" , strength: 1 },
{ target: "d4:BM", source: "d4" , strength: 1 },
{ target: "d5,d6:BM", source: "d5" , strength: 1 },
{ target: "d6:BM", source: "d6" , strength: 1 },
{ target: "t1,t2:BI", source: "t1" , strength: 1 },
{ target: "t1,t2:BI", source: "t2" , strength: 1 },
{ target: "t3:BI", source: "t3" , strength: 1 },
{ target: "t4:BI", source: "t4" , strength: 1 },
{ target: "t5,t6:BI", source: "t5" , strength: 1 },
{ target: "t5,t6:BI", source: "t6" , strength: 1 },
{ target: "t1,t2:BM", source: "t1" , strength: 1 },
{ target: "t1,t2:BM", source: "t2" , strength: 1 },
{ target: "t3:BM", source: "t3" , strength: 1 },
{ target: "t4:BM", source: "t4" , strength: 1 },
{ target: "t5,t6:BM", source: "t5" , strength: 1 },
{ target: "t6:BM", source: "t6" , strength: 1 }
]
var nodes = [...baseNodes]
var links = [...baseLinks]
function getNeighbors(node) {
return baseLinks.reduce(function (neighbors, link) {
if (link.target.id === node.id) {
neighbors.push(link.source.id)
} else if (link.source.id === node.id) {
neighbors.push(link.target.id)
}
return neighbors
},
[node.id]
)
}
function isNeighborLink(node, link) {
return link.target.id === node.id || link.source.id === node.id
}
function getNodeColor(node, neighbors) {
if (Array.isArray(neighbors) && neighbors.indexOf(node.id) > -1) {
return node.level === 1 ? 'blue' : 'green'
}
return node.level === 1 ? 'red' : 'gray'
}
function getLinkColor(node, link) {
return isNeighborLink(node, link) ? 'green' : '#E5E5E5'
}
function getTextColor(node, neighbors) {
return Array.isArray(neighbors) && neighbors.indexOf(node.id) > -1 ? 'green' : 'black'
}
var width = window.innerWidth
var height = window.innerHeight
var svg = d3.select('svg')
svg.attr('width', width).attr('height', height)
var linkElements,
nodeElements,
textElements
// we use svg groups to logically group the elements together
var linkGroup = svg.append('g').attr('class', 'links')
var nodeGroup = svg.append('g').attr('class', 'nodes')
var textGroup = svg.append('g').attr('class', 'texts')
// we use this reference to select/deselect
// after clicking the same element twice
var selectedId
// simulation setup with all forces
var linkForce = d3
.forceLink()
.id(function (link) { return link.id })
.strength(function (link) { return link.strength })
var simulation = d3
.forceSimulation()
simulation.force('link', linkForce)
.force('charge', d3.forceManyBody().strength(-400))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('forceX', d3.forceX())
.force('forceY', d3.forceY())
simulation.force('forceY')
.y(height+0.5)
.strength(0.1)
simulation.force('forceX')
.x(width+0.5)
.strength(0.1);
// 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()
}
var neighbors = getNeighbors(selectedNode)
// we modify the styles to highlight selected nodes
nodeElements.attr('fill', function (node) { return getNodeColor(node, neighbors) })
textElements.attr('fill', function (node) { return getTextColor(node, neighbors) })
linkElements.attr('stroke', function (link) { return getLinkColor(selectedNode, link) })
}
// this helper simple adds all nodes and links
// that are missing, to recreate the initial state
function resetData() {
var nodeIds = nodes.map(function (node) { return node.id })
baseNodes.forEach(function (node) {
if (nodeIds.indexOf(node.id) === -1) {
nodes.push(node)
}
})
links = baseLinks
}
// diffing and mutating the data
function updateData(selectedNode) {
var neighbors = getNeighbors(selectedNode)
var newNodes = baseNodes.filter(function (node) {
return neighbors.indexOf(node.id) > -1 || node.level === 1
})
var diff = {
removed: nodes.filter(function (node) { return newNodes.indexOf(node) === -1 }),
added: newNodes.filter(function (node) { return nodes.indexOf(node) === -1 })
}
diff.removed.forEach(function (node) { nodes.splice(nodes.indexOf(node), 1) })
diff.added.forEach(function (node) { nodes.push(node) })
links = baseLinks.filter(function (link) {
return link.target.id === selectedNode.id || link.source.id === selectedNode.id
})
}
function updateGraph() {
// links
linkElements = linkGroup.selectAll('line')
.data(links, function (link) {
return link.target.id + link.source.id
})
linkElements.exit().remove()
var 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, function (node) { return node.id })
nodeElements.exit().remove()
var s = d3.scaleOrdinal(d3.schemeCategory20)
var nodeEnter = nodeElements
.enter()
.append('circle')
.attr('r', function(node) {
return 18-(node.level*3)
})
.attr('fill', function (node) {
return s(node.level)
})
nodeElements = nodeEnter.merge(nodeElements)
// texts
textElements = textGroup.selectAll('text')
.data(nodes, function (node) { return node.id })
textElements.exit().remove()
var textEnter = textElements
.enter()
.append('text')
.text(function (node) { return node.label })
.attr('font-family', 'Helvetica')
.attr('font-size', 13)
.attr('fill', '#555555')
.attr('dx', 15)
.attr('dy', 4)
textElements = textEnter.merge(textElements)
}
function updateSimulation() {
updateGraph()
simulation.nodes(nodes).on('tick', () => {
nodeElements
.attr('cx', function (node) { return node.x })
.attr('cy', function (node) { return node.y })
textElements
.attr('x', function (node) { return node.x })
.attr('y', function (node) { return node.y })
linkElements
.attr('x1', function (link) { return link.source.x })
.attr('y1', function (link) { return link.source.y })
.attr('x2', function (link) { return link.target.x })
.attr('y2', function (link) { return link.target.y })
})
simulation.force('link').links(links)
// simulation.alphaTarget(1).restart()
}
// last but not least, we call updateSimulation
// to trigger the initial render
updateSimulation()
</script>
https://d3js.org/d3.v4.min.js