The bmannconsulting.com website
1<style> 2 .links line { 3 stroke: #ccc; 4 opacity: 0.5; 5 } 6 7 .nodes circle { 8 cursor: pointer; 9 fill: #8b88e6; 10 transition: all 0.15s ease-out; 11 } 12 13 .text text { 14 cursor: pointer; 15 fill: #333; 16 text-shadow: -1px -1px 0 #fafafabb, 1px -1px 0 #fafafabb, -1px 1px 0 #fafafabb, 1px 1px 0 #fafafabb; 17 } 18 19 .nodes [active], 20 .text [active] { 21 cursor: pointer; 22 fill: black; 23 } 24 25 .inactive { 26 opacity: 0.1; 27 transition: all 0.15s ease-out; 28 } 29 30 #graph-wrapper { 31 background: #fcfcfc; 32 border-radius: 4px; 33 height: auto; 34 } 35 36 #graph-wrapper > svg { 37 max-width: 100%; 38 display: block; 39 } 40</style> 41 42<div id="graph-wrapper"> 43 <script> 44 window.addEventListener("load", loadGraph); 45 46 function loadGraph() { 47 var oScript = document.createElement("script"); 48 oScript.src = "https://cdnjs.cloudflare.com/ajax/libs/d3/5.16.0/d3.min.js"; 49 oScript.crossOrigin = 'anonymous'; 50 oScript.integrity = 51 "sha512-FHsFVKQ/T1KWJDGSbrUhTJyS1ph3eRrxI228ND0EGaEp6v4a/vGwPWd3Dtd/+9cI7ccofZvl/wulICEurHN1pg=="; 52 document.body.appendChild(oScript); 53 oScript.onload = () => { 54 const MINIMAL_NODE_SIZE = 8; 55 const MAX_NODE_SIZE = 12; 56 const ACTIVE_RADIUS_FACTOR = 1.5; 57 const STROKE = 1; 58 const FONT_SIZE = 16; 59 const TICKS = 200; 60 const FONT_BASELINE = 40; 61 const MAX_LABEL_LENGTH = 50; 62 63 const graphData = {% include notes_graph.json %} 64 let nodesData = graphData.nodes; 65 let linksData = graphData.edges; 66 67 const nodeSize = {}; 68 69 const updateNodeSize = () => { 70 nodesData.forEach((el) => { 71 let weight = 72 3 * 73 Math.sqrt( 74 linksData.filter((l) => l.source.id === el.id || l.target.id === el.id) 75 .length + 1 76 ); 77 if (weight < MINIMAL_NODE_SIZE) { 78 weight = MINIMAL_NODE_SIZE; 79 } else if (weight > MAX_NODE_SIZE) { 80 weight = MAX_NODE_SIZE; 81 } 82 nodeSize[el.id] = weight; 83 }); 84 }; 85 86 const onClick = (d) => { 87 window.location = d.path 88 }; 89 90 const onMouseover = function (d) { 91 const relatedNodesSet = new Set(); 92 linksData 93 .filter((n) => n.target.id == d.id || n.source.id == d.id) 94 .forEach((n) => { 95 relatedNodesSet.add(n.target.id); 96 relatedNodesSet.add(n.source.id); 97 }); 98 99 node.attr("class", (node_d) => { 100 if (node_d.id !== d.id && !relatedNodesSet.has(node_d.id)) { 101 return "inactive"; 102 } 103 return ""; 104 }); 105 106 link.attr("class", (link_d) => { 107 if (link_d.source.id !== d.id && link_d.target.id !== d.id) { 108 return "inactive"; 109 } 110 return ""; 111 }); 112 113 link.attr("stroke-width", (link_d) => { 114 if (link_d.source.id === d.id || link_d.target.id === d.id) { 115 return STROKE * 4; 116 } 117 return STROKE; 118 }); 119 text.attr("class", (text_d) => { 120 if (text_d.id !== d.id && !relatedNodesSet.has(text_d.id)) { 121 return "inactive"; 122 } 123 return ""; 124 }); 125 }; 126 127 const onMouseout = function (d) { 128 node.attr("class", ""); 129 link.attr("class", ""); 130 text.attr("class", ""); 131 link.attr("stroke-width", STROKE); 132 }; 133 134 const sameNodes = (previous, next) => { 135 if (next.length !== previous.length) { 136 return false; 137 } 138 139 const map = new Map(); 140 for (const node of previous) { 141 map.set(node.id, node.label); 142 } 143 144 for (const node of next) { 145 const found = map.get(node.id); 146 if (!found || found !== node.title) { 147 return false; 148 } 149 } 150 151 return true; 152 }; 153 154 const sameEdges = (previous, next) => { 155 if (next.length !== previous.length) { 156 return false; 157 } 158 159 const set = new Set(); 160 for (const edge of previous) { 161 set.add(`${edge.source.id}-${edge.target.id}`); 162 } 163 164 for (const edge of next) { 165 if (!set.has(`${edge.source.id}-${edge.target.id}`)) { 166 return false; 167 } 168 } 169 170 return true; 171 }; 172 173 const graphWrapper = document.getElementById('graph-wrapper') 174 const element = document.createElementNS("http://www.w3.org/2000/svg", "svg"); 175 element.setAttribute("width", graphWrapper.getBoundingClientRect().width); 176 element.setAttribute("height", window.innerHeight * 0.8); 177 graphWrapper.appendChild(element); 178 179 const reportWindowSize = () => { 180 element.setAttribute("width", window.innerWidth); 181 element.setAttribute("height", window.innerHeight); 182 }; 183 184 window.onresize = reportWindowSize; 185 186 const svg = d3.select("svg"); 187 const width = Number(svg.attr("width")); 188 const height = Number(svg.attr("height")); 189 let zoomLevel = 1; 190 191 const simulation = d3 192 .forceSimulation(nodesData) 193 .force("forceX", d3.forceX().x(width / 2)) 194 .force("forceY", d3.forceY().y(height / 2)) 195 .force("charge", d3.forceManyBody()) 196 .force( 197 "link", 198 d3 199 .forceLink(linksData) 200 .id((d) => d.id) 201 .distance(70) 202 ) 203 .force("center", d3.forceCenter(width / 2, height / 2)) 204 .force("collision", d3.forceCollide().radius(80)) 205 .stop(); 206 207 const g = svg.append("g"); 208 let link = g.append("g").attr("class", "links").selectAll(".link"); 209 let node = g.append("g").attr("class", "nodes").selectAll(".node"); 210 let text = g.append("g").attr("class", "text").selectAll(".text"); 211 212 const resize = () => { 213 if (d3.event) { 214 const scale = d3.event.transform; 215 zoomLevel = scale.k; 216 g.attr("transform", scale); 217 } 218 219 const zoomOrKeep = (value) => (zoomLevel >= 1 ? value / zoomLevel : value); 220 221 const font = Math.max(Math.round(zoomOrKeep(FONT_SIZE)), 1); 222 223 text.attr("font-size", (d) => font); 224 text.attr("y", (d) => d.y - zoomOrKeep(FONT_BASELINE) + 8); 225 link.attr("stroke-width", zoomOrKeep(STROKE)); 226 node.attr("r", (d) => { 227 return zoomOrKeep(nodeSize[d.id]); 228 }); 229 svg 230 .selectAll("circle") 231 .filter((_d, i, nodes) => d3.select(nodes[i]).attr("active")) 232 .attr("r", (d) => zoomOrKeep(ACTIVE_RADIUS_FACTOR * nodeSize[d.id])); 233 }; 234 235 const ticked = () => { 236 node.attr("cx", (d) => d.x).attr("cy", (d) => d.y); 237 text 238 .attr("x", (d) => d.x) 239 .attr("y", (d) => d.y - (FONT_BASELINE - nodeSize[d.id]) / zoomLevel); 240 link 241 .attr("x1", (d) => d.source.x) 242 .attr("y1", (d) => d.source.y) 243 .attr("x2", (d) => d.target.x) 244 .attr("y2", (d) => d.target.y); 245 }; 246 247 const restart = () => { 248 updateNodeSize(); 249 node = node.data(nodesData, (d) => d.id); 250 node.exit().remove(); 251 node = node 252 .enter() 253 .append("circle") 254 .attr("r", (d) => { 255 return nodeSize[d.id]; 256 }) 257 .on("click", onClick) 258 .on("mouseover", onMouseover) 259 .on("mouseout", onMouseout) 260 .merge(node); 261 262 link = link.data(linksData, (d) => `${d.source.id}-${d.target.id}`); 263 link.exit().remove(); 264 link = link.enter().append("line").attr("stroke-width", STROKE).merge(link); 265 266 text = text.data(nodesData, (d) => d.label); 267 text.exit().remove(); 268 text = text 269 .enter() 270 .append("text") 271 .text((d) => shorten(d.label.replace(/_*/g, ""), MAX_LABEL_LENGTH)) 272 .attr("font-size", `${FONT_SIZE}px`) 273 .attr("text-anchor", "middle") 274 .attr("alignment-baseline", "central") 275 .on("click", onClick) 276 .on("mouseover", onMouseover) 277 .on("mouseout", onMouseout) 278 .merge(text); 279 280 node.attr("active", (d) => isCurrentPath(d.path) ? true : null); 281 text.attr("active", (d) => isCurrentPath(d.path) ? true : null); 282 283 simulation.nodes(nodesData); 284 simulation.force("link").links(linksData); 285 simulation.alpha(1).restart(); 286 simulation.stop(); 287 288 for (let i = 0; i < TICKS; i++) { 289 simulation.tick(); 290 } 291 292 ticked(); 293 }; 294 295 const zoomHandler = d3.zoom().scaleExtent([0.2, 3]).on("zoom", resize); 296 297 zoomHandler(svg); 298 restart(); 299 300 function isCurrentPath(notePath) { 301 return window.location.pathname.includes(notePath) 302 } 303 304 function shorten(str, maxLen, separator = ' ') { 305 if (str.length <= maxLen) return str; 306 return str.substr(0, str.lastIndexOf(separator, maxLen)) + '...'; 307 } 308 } 309 } 310 </script> 311</div>