forked from rofrischmann's block: Dynamic Updates
forked from anonymous's block: Force Directed Graph
| license: mit |
forked from rofrischmann's block: Dynamic Updates
forked from anonymous's block: Force Directed Graph
| <!DOCTYPE html> | |
| <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> |