Add notes graph visualization
This commit is contained in:
		
							parent
							
								
									b6849b30aa
								
							
						
					
					
						commit
						e0005f58ba
					
				
							
								
								
									
										196
									
								
								_includes/notes_graph.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										196
									
								
								_includes/notes_graph.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,196 @@ | |||||||
|  | <style> | ||||||
|  |   .links line { | ||||||
|  |     stroke: #ccc; | ||||||
|  |     opacity: 0.5; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .nodes circle { | ||||||
|  |     cursor: pointer; | ||||||
|  |     fill: blue; | ||||||
|  |     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: red; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .nodes circle[active] { | ||||||
|  |     r: 6; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   #graph-wrapper { | ||||||
|  |     background: #fafafa; | ||||||
|  |     border-radius: 4px; | ||||||
|  |     height: auto; | ||||||
|  |   } | ||||||
|  | </style> | ||||||
|  | 
 | ||||||
|  | <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.16.0/d3.min.js" | ||||||
|  |   integrity="sha512-FHsFVKQ/T1KWJDGSbrUhTJyS1ph3eRrxI228ND0EGaEp6v4a/vGwPWd3Dtd/+9cI7ccofZvl/wulICEurHN1pg==" | ||||||
|  |   crossorigin="anonymous"></script> | ||||||
|  | 
 | ||||||
|  | <div id="graph-wrapper"> | ||||||
|  |   <script> | ||||||
|  |     const RADIUS = 4; | ||||||
|  |     const STROKE = 1; | ||||||
|  |     const FONT_SIZE = 15; | ||||||
|  |     const TICKS = 100; | ||||||
|  |     const FONT_BASELINE = 15; | ||||||
|  |     const MAX_LABEL_LENGTH = 50; | ||||||
|  | 
 | ||||||
|  |     const graphData = {% include notes_graph.json %} | ||||||
|  | 
 | ||||||
|  |     let nodesData = graphData.nodes; | ||||||
|  |     let linksData = graphData.edges; | ||||||
|  | 
 | ||||||
|  |     const onClick = (d) => { | ||||||
|  |       window.location = d.path | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const element = document.createElementNS( | ||||||
|  |       "http://www.w3.org/2000/svg", | ||||||
|  |       "svg" | ||||||
|  |     ); | ||||||
|  |     const graphWrapper = document.getElementById('graph-wrapper') | ||||||
|  | 
 | ||||||
|  |     element.setAttribute("width", graphWrapper.getBoundingClientRect().width); | ||||||
|  |     graphWrapper.setAttribute("height", window.innerHeight * 0.8); | ||||||
|  |     element.setAttribute("height", window.innerHeight * 0.8); | ||||||
|  | 
 | ||||||
|  |     graphWrapper.appendChild(element); | ||||||
|  | 
 | ||||||
|  |     const reportWindowSize = () => { | ||||||
|  |       element.setAttribute("width", graphWrapper.getBoundingClientRect().width); | ||||||
|  |       graphWrapper.setAttribute("height", window.innerHeight * 0.8); | ||||||
|  |       element.setAttribute("height", window.innerHeight * 0.8); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     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("charge", d3 | ||||||
|  |         .forceManyBody() | ||||||
|  |         .strength(-4000) | ||||||
|  |       ) | ||||||
|  |       .force( | ||||||
|  |         "link", | ||||||
|  |         d3 | ||||||
|  |           .forceLink(linksData) | ||||||
|  |           .id((d) => d.id) | ||||||
|  |           .distance(150) | ||||||
|  |       ) | ||||||
|  |       .force("center", d3.forceCenter(width / 2, height / 2)) | ||||||
|  |       .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 zoomActions = () => { | ||||||
|  |       const scale = d3.event.transform; | ||||||
|  |       zoomLevel = scale.k; | ||||||
|  |       g.attr("transform", scale); | ||||||
|  | 
 | ||||||
|  |       const zoomOrKeep = (value) => (scale.k >= 1 ? value / scale.k : value); | ||||||
|  | 
 | ||||||
|  |       const font = Math.max(Math.round(zoomOrKeep(FONT_SIZE)), 1); | ||||||
|  | 
 | ||||||
|  |       text.attr("font-size", `${font}px`); | ||||||
|  |       text.attr("y", (d) => d.y - zoomOrKeep(FONT_BASELINE)); | ||||||
|  |       link.attr("stroke-width", zoomOrKeep(STROKE)); | ||||||
|  |       node.attr("r", zoomOrKeep(RADIUS)); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     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 / 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 = () => { | ||||||
|  |       node = node.data(nodesData, (d) => d.id); | ||||||
|  |       node.exit().remove(); | ||||||
|  |       node = node | ||||||
|  |         .enter() | ||||||
|  |         .append("circle") | ||||||
|  |         .attr("r", RADIUS) | ||||||
|  |         // .attr("fill", (d) => getNodeColor(d)) | ||||||
|  |         .on("click", onClick) | ||||||
|  |         .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); | ||||||
|  | 
 | ||||||
|  |       node.attr("active", (d) => isCurrentPath(d.path) ? true : null); | ||||||
|  |       text.attr("active", (d) => isCurrentPath(d.path) ? true : null); | ||||||
|  | 
 | ||||||
|  |       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) | ||||||
|  |         .merge(text); | ||||||
|  | 
 | ||||||
|  |       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]) | ||||||
|  |       //.translateExtent([[0,0], [width, height]]) | ||||||
|  |       //.extent([[0, 0], [width, height]]) | ||||||
|  |       .on("zoom", zoomActions); | ||||||
|  | 
 | ||||||
|  |     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)) + '...'; | ||||||
|  |     } | ||||||
|  |   </script> | ||||||
|  | </div> | ||||||
| @ -39,3 +39,8 @@ layout: default | |||||||
|   </div> |   </div> | ||||||
| </article> | </article> | ||||||
| 
 | 
 | ||||||
|  | <hr> | ||||||
|  | 
 | ||||||
|  | <p>Here are all the notes in this garden, along with their links, visualized as a graph.</p> | ||||||
|  | 
 | ||||||
|  | {% include notes_graph.html %} | ||||||
|  | |||||||
| @ -8,3 +8,7 @@ permalink: / | |||||||
| # Welcome! 🌱 | # Welcome! 🌱 | ||||||
| 
 | 
 | ||||||
| This is your digital garden. Here's [[Your first seed]] to get started on your exploration. | This is your digital garden. Here's [[Your first seed]] to get started on your exploration. | ||||||
|  | 
 | ||||||
|  | <p>Here are all the notes in this garden, along with their links, visualized as a graph.</p> | ||||||
|  | 
 | ||||||
|  | {% include notes_graph.html %} | ||||||
|  | |||||||
| @ -1,6 +1,9 @@ | |||||||
| # frozen_string_literal: true | # frozen_string_literal: true | ||||||
| class BidirectionalLinksGenerator < Jekyll::Generator | class BidirectionalLinksGenerator < Jekyll::Generator | ||||||
|   def generate(site) |   def generate(site) | ||||||
|  |     graph_nodes = [] | ||||||
|  |     graph_edges = [] | ||||||
|  | 
 | ||||||
|     all_notes = site.collections['notes'].docs |     all_notes = site.collections['notes'].docs | ||||||
|     all_pages = site.pages |     all_pages = site.pages | ||||||
| 
 | 
 | ||||||
| @ -19,11 +22,37 @@ class BidirectionalLinksGenerator < Jekyll::Generator | |||||||
| 
 | 
 | ||||||
|     # Identify note backlinks and add them to each note |     # Identify note backlinks and add them to each note | ||||||
|     all_notes.each do |current_note| |     all_notes.each do |current_note| | ||||||
|  | 			# Nodes: Jekyll | ||||||
|       notes_linking_to_current_note = all_notes.filter do |e| |       notes_linking_to_current_note = all_notes.filter do |e| | ||||||
|         e.content.include?(current_note.url) |         e.content.include?(current_note.url) | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  |       # Nodes: Graph | ||||||
|  |       graph_nodes << { | ||||||
|  |         id: note_id_from_note(current_note), | ||||||
|  |         path: current_note.url, | ||||||
|  |         label: current_note.title, | ||||||
|  |       } unless current_note.path.include?('_notes/index.html') | ||||||
|  | 
 | ||||||
|  | 			# Edges: Jekyll | ||||||
|       current_note.data['backlinks'] = notes_linking_to_current_note |       current_note.data['backlinks'] = notes_linking_to_current_note | ||||||
|  | 
 | ||||||
|  |       # Edges: Graph | ||||||
|  |       notes_linking_to_current_note.each do |n| | ||||||
|  |         graph_edges << { | ||||||
|  |           source: note_id_from_note(n), | ||||||
|  |           target: note_id_from_note(current_note), | ||||||
|  |         } | ||||||
|  |       end | ||||||
|     end |     end | ||||||
|  | 
 | ||||||
|  |     File.write('_includes/notes_graph.json', JSON.dump({ | ||||||
|  |       edges: graph_edges, | ||||||
|  |       nodes: graph_nodes, | ||||||
|  |     })) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def note_id_from_note(note) | ||||||
|  |     note.title.to_i(36).to_s | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Maxime Vaillancourt
						Maxime Vaillancourt