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>