316 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
			
		
		
	
	
			316 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
| <style>
 | |
|   .links line {
 | |
|     stroke: #ccc;
 | |
|     opacity: 0.5;
 | |
|   }
 | |
| 
 | |
|   .nodes circle {
 | |
|     cursor: pointer;
 | |
|     fill: #8b88e6;
 | |
|     transition: all 0.15s ease-out;
 | |
|   }
 | |
| 
 | |
|   .text text {
 | |
|     cursor: pointer;
 | |
|     fill: #333;
 | |
|     text-shadow: -1px -1px 0 #fafafabb, 1px -1px 0 #fafafabb, -1px 1px 0 #fafafabb, 1px 1px 0 #fafafabb;
 | |
|   }
 | |
| 
 | |
|   .nodes [active],
 | |
|   .text [active] {
 | |
|     cursor: pointer;
 | |
|     fill: black;
 | |
|   }
 | |
| 
 | |
|   .inactive {
 | |
|     opacity: 0.1;
 | |
|     transition: all 0.15s ease-out;
 | |
|   }
 | |
| 
 | |
|   #graph-wrapper {
 | |
|     background: #fcfcfc;
 | |
|     border-radius: 4px;
 | |
|     height: auto;
 | |
|   }
 | |
| </style>
 | |
| 
 | |
| <div id="graph-wrapper">
 | |
|   <script>
 | |
|     var commentFlag = true;
 | |
|     
 | |
|     window.addEventListener("load", loadGraph);
 | |
|     window.addEventListener("scroll", loadGraph);
 | |
| 
 | |
|     function loadGraph() {
 | |
|       if (!( document.getElementById("graph-wrapper").getBoundingClientRect().top <
 | |
|              window.innerHeight * 1.5 &&
 | |
|           commentFlag)){
 | |
|         return;
 | |
|       }
 | |
|       var oScript = document.createElement("script");
 | |
|       oScript.src = "https://cdnjs.cloudflare.com/ajax/libs/d3/5.16.0/d3.min.js";
 | |
|       oScript.crossOrigin = 'anonymous';
 | |
|       oScript.integrity =
 | |
|         "sha512-FHsFVKQ/T1KWJDGSbrUhTJyS1ph3eRrxI228ND0EGaEp6v4a/vGwPWd3Dtd/+9cI7ccofZvl/wulICEurHN1pg==";
 | |
|       document.body.appendChild(oScript);
 | |
|       oScript.onload = () => {
 | |
|         const MINIMAL_NODE_SIZE = 8;
 | |
|         const MAX_NODE_SIZE = 12;
 | |
|         const ACTIVE_RADIUS_FACTOR = 1.5;
 | |
|         const STROKE = 1;
 | |
|         const FONT_SIZE = 16;
 | |
|         const TICKS = 200;
 | |
|         const FONT_BASELINE = 40;
 | |
|         const MAX_LABEL_LENGTH = 50;
 | |
| 
 | |
|         const graphData = {% include notes_graph.json %}
 | |
|         let nodesData = graphData.nodes;
 | |
|         let linksData = graphData.edges;
 | |
| 
 | |
|         const nodeSize = {};
 | |
| 
 | |
|         const updateNodeSize = () => {
 | |
|           nodesData.forEach((el) => {
 | |
|             let weight =
 | |
|               3 *
 | |
|               Math.sqrt(
 | |
|                 linksData.filter((l) => l.source.id === el.id || l.target.id === el.id)
 | |
|                   .length + 1
 | |
|               );
 | |
|             if (weight < MINIMAL_NODE_SIZE) {
 | |
|               weight = MINIMAL_NODE_SIZE;
 | |
|             } else if (weight > MAX_NODE_SIZE) {
 | |
|               weight = MAX_NODE_SIZE;
 | |
|             }
 | |
|             nodeSize[el.id] = weight;
 | |
|           });
 | |
|         };
 | |
| 
 | |
|         const onClick = (d) => {
 | |
|           window.location = d.path
 | |
|         };
 | |
| 
 | |
|         const onMouseover = function (d) {
 | |
|           const relatedNodesSet = new Set();
 | |
|           linksData
 | |
|             .filter((n) => n.target.id == d.id || n.source.id == d.id)
 | |
|             .forEach((n) => {
 | |
|               relatedNodesSet.add(n.target.id);
 | |
|               relatedNodesSet.add(n.source.id);
 | |
|             });
 | |
| 
 | |
|           node.attr("class", (node_d) => {
 | |
|             if (node_d.id !== d.id && !relatedNodesSet.has(node_d.id)) {
 | |
|               return "inactive";
 | |
|             }
 | |
|             return "";
 | |
|           });
 | |
| 
 | |
|           link.attr("class", (link_d) => {
 | |
|             if (link_d.source.id !== d.id && link_d.target.id !== d.id) {
 | |
|               return "inactive";
 | |
|             }
 | |
|             return "";
 | |
|           });
 | |
| 
 | |
|           link.attr("stroke-width", (link_d) => {
 | |
|             if (link_d.source.id === d.id || link_d.target.id === d.id) {
 | |
|               return STROKE * 4;
 | |
|             }
 | |
|             return STROKE;
 | |
|           });
 | |
|           text.attr("class", (text_d) => {
 | |
|             if (text_d.id !== d.id && !relatedNodesSet.has(text_d.id)) {
 | |
|               return "inactive";
 | |
|             }
 | |
|             return "";
 | |
|           });
 | |
|         };
 | |
| 
 | |
|         const onMouseout = function (d) {
 | |
|           node.attr("class", "");
 | |
|           link.attr("class", "");
 | |
|           text.attr("class", "");
 | |
|           link.attr("stroke-width", STROKE);
 | |
|         };
 | |
| 
 | |
|         const sameNodes = (previous, next) => {
 | |
|           if (next.length !== previous.length) {
 | |
|             return false;
 | |
|           }
 | |
| 
 | |
|           const map = new Map();
 | |
|           for (const node of previous) {
 | |
|             map.set(node.id, node.label);
 | |
|           }
 | |
| 
 | |
|           for (const node of next) {
 | |
|             const found = map.get(node.id);
 | |
|             if (!found || found !== node.title) {
 | |
|               return false;
 | |
|             }
 | |
|           }
 | |
| 
 | |
|           return true;
 | |
|         };
 | |
| 
 | |
|         const sameEdges = (previous, next) => {
 | |
|           if (next.length !== previous.length) {
 | |
|             return false;
 | |
|           }
 | |
| 
 | |
|           const set = new Set();
 | |
|           for (const edge of previous) {
 | |
|             set.add(`${edge.source.id}-${edge.target.id}`);
 | |
|           }
 | |
| 
 | |
|           for (const edge of next) {
 | |
|             if (!set.has(`${edge.source.id}-${edge.target.id}`)) {
 | |
|               return false;
 | |
|             }
 | |
|           }
 | |
| 
 | |
|           return true;
 | |
|         };
 | |
| 
 | |
|         const graphWrapper = document.getElementById('graph-wrapper')
 | |
|         const element = document.createElementNS("http://www.w3.org/2000/svg", "svg");
 | |
|         element.setAttribute("width", graphWrapper.getBoundingClientRect().width);
 | |
|         element.setAttribute("height", window.innerHeight * 0.8);
 | |
|         graphWrapper.appendChild(element);
 | |
| 
 | |
|         const reportWindowSize = () => {
 | |
|           element.setAttribute("width", window.innerWidth);
 | |
|           element.setAttribute("height", window.innerHeight);
 | |
|         };
 | |
| 
 | |
|         window.onresize = reportWindowSize;
 | |
| 
 | |
|         const svg = d3.select("svg");
 | |
|         const width = Number(svg.attr("width"));
 | |
|         const height = Number(svg.attr("height"));
 | |
|         let zoomLevel = 1;
 | |
| 
 | |
|         const simulation = d3
 | |
|           .forceSimulation(nodesData)
 | |
|           .force("forceX", d3.forceX().x(width / 2))
 | |
|           .force("forceY", d3.forceY().y(height / 2))
 | |
|           .force("charge", d3.forceManyBody())
 | |
|           .force(
 | |
|             "link",
 | |
|             d3
 | |
|               .forceLink(linksData)
 | |
|               .id((d) => d.id)
 | |
|               .distance(70)
 | |
|           )
 | |
|           .force("center", d3.forceCenter(width / 2, height / 2))
 | |
|           .force("collision", d3.forceCollide().radius(80))
 | |
|           .stop();
 | |
| 
 | |
|         const g = svg.append("g");
 | |
|         let link = g.append("g").attr("class", "links").selectAll(".link");
 | |
|         let node = g.append("g").attr("class", "nodes").selectAll(".node");
 | |
|         let text = g.append("g").attr("class", "text").selectAll(".text");
 | |
| 
 | |
|         const resize = () => {
 | |
|           if (d3.event) {
 | |
|             const scale = d3.event.transform;
 | |
|             zoomLevel = scale.k;
 | |
|             g.attr("transform", scale);
 | |
|           }
 | |
| 
 | |
|           const zoomOrKeep = (value) => (zoomLevel >= 1 ? value / zoomLevel : value);
 | |
| 
 | |
|           const font = Math.max(Math.round(zoomOrKeep(FONT_SIZE)), 1);
 | |
| 
 | |
|           text.attr("font-size", (d) => font);
 | |
|           text.attr("y", (d) => d.y - zoomOrKeep(FONT_BASELINE) + 8);
 | |
|           link.attr("stroke-width", zoomOrKeep(STROKE));
 | |
|           node.attr("r", (d) => {
 | |
|             return zoomOrKeep(nodeSize[d.id]);
 | |
|           });
 | |
|           svg
 | |
|             .selectAll("circle")
 | |
|             .filter((_d, i, nodes) => d3.select(nodes[i]).attr("active"))
 | |
|             .attr("r", (d) => zoomOrKeep(ACTIVE_RADIUS_FACTOR * nodeSize[d.id]));
 | |
|         };
 | |
| 
 | |
|         const ticked = () => {
 | |
|           node.attr("cx", (d) => d.x).attr("cy", (d) => d.y);
 | |
|           text
 | |
|             .attr("x", (d) => d.x)
 | |
|             .attr("y", (d) => d.y - (FONT_BASELINE - nodeSize[d.id]) / zoomLevel);
 | |
|           link
 | |
|             .attr("x1", (d) => d.source.x)
 | |
|             .attr("y1", (d) => d.source.y)
 | |
|             .attr("x2", (d) => d.target.x)
 | |
|             .attr("y2", (d) => d.target.y);
 | |
|         };
 | |
| 
 | |
|         const restart = () => {
 | |
|           updateNodeSize();
 | |
|           node = node.data(nodesData, (d) => d.id);
 | |
|           node.exit().remove();
 | |
|           node = node
 | |
|             .enter()
 | |
|             .append("circle")
 | |
|             .attr("r", (d) => {
 | |
|               return nodeSize[d.id];
 | |
|             })
 | |
|             .on("click", onClick)
 | |
|             .on("mouseover", onMouseover)
 | |
|             .on("mouseout", onMouseout)
 | |
|             .merge(node);
 | |
| 
 | |
|           link = link.data(linksData, (d) => `${d.source.id}-${d.target.id}`);
 | |
|           link.exit().remove();
 | |
|           link = link.enter().append("line").attr("stroke-width", STROKE).merge(link);
 | |
| 
 | |
|           text = text.data(nodesData, (d) => d.label);
 | |
|           text.exit().remove();
 | |
|           text = text
 | |
|             .enter()
 | |
|             .append("text")
 | |
|             .text((d) => shorten(d.label.replace(/_*/g, ""), MAX_LABEL_LENGTH))
 | |
|             .attr("font-size", `${FONT_SIZE}px`)
 | |
|             .attr("text-anchor", "middle")
 | |
|             .attr("alignment-baseline", "central")
 | |
|             .on("click", onClick)
 | |
|             .on("mouseover", onMouseover)
 | |
|             .on("mouseout", onMouseout)
 | |
|             .merge(text);
 | |
| 
 | |
|           node.attr("active", (d) => isCurrentPath(d.path) ? true : null);
 | |
|           text.attr("active", (d) => isCurrentPath(d.path) ? true : null);
 | |
| 
 | |
|           simulation.nodes(nodesData);
 | |
|           simulation.force("link").links(linksData);
 | |
|           simulation.alpha(1).restart();
 | |
|           simulation.stop();
 | |
| 
 | |
|           for (let i = 0; i < TICKS; i++) {
 | |
|             simulation.tick();
 | |
|           }
 | |
| 
 | |
|           ticked();
 | |
|         };
 | |
| 
 | |
|         const zoomHandler = d3.zoom().scaleExtent([0.2, 3]).on("zoom", resize);
 | |
| 
 | |
|         zoomHandler(svg);
 | |
|         restart();
 | |
| 
 | |
|         function isCurrentPath(notePath) {
 | |
|           return window.location.pathname.includes(notePath)
 | |
|         }
 | |
| 
 | |
|         function shorten(str, maxLen, separator = ' ') {
 | |
|           if (str.length <= maxLen) return str;
 | |
|           return str.substr(0, str.lastIndexOf(separator, maxLen)) + '...';
 | |
|         }
 | |
|         commentFlag = false;
 | |
|       }
 | |
|     }
 | |
|   </script>
 | |
| </div>
 | 
